Torkel Ödegaard 8 лет назад
Родитель
Сommit
c42a232644
54 измененных файлов с 1489 добавлено и 660 удалено
  1. 2 1
      jest.config.js
  2. 12 8
      package.json
  3. 2 2
      pkg/api/alerting.go
  4. 1 1
      pkg/services/sqlstore/alert.go
  5. 1 1
      pkg/services/sqlstore/stats.go
  6. 3 0
      public/app/app.ts
  7. 68 0
      public/app/containers/AlertRuleList/AlertRuleList.jest.tsx
  8. 145 0
      public/app/containers/AlertRuleList/AlertRuleList.tsx
  9. 72 0
      public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap
  10. 15 0
      public/app/containers/IContainerProps.ts
  11. 30 0
      public/app/containers/ServerStats/ServerStats.jest.tsx
  12. 45 0
      public/app/containers/ServerStats/ServerStats.tsx
  13. 170 0
      public/app/containers/ServerStats/__snapshots__/ServerStats.jest.tsx.snap
  14. 11 9
      public/app/core/angular_wrappers.ts
  15. 12 27
      public/app/core/components/PageHeader/PageHeader.tsx
  16. 7 64
      public/app/core/components/grafana_app.ts
  17. 86 0
      public/app/core/components/search/SearchResult.tsx
  18. 4 2
      public/app/core/components/sidemenu/sidemenu.ts
  19. 0 1
      public/app/core/core.ts
  20. 0 26
      public/app/core/routes/bundle_loader.ts
  21. 1 1
      public/app/core/services/all.js
  22. 32 6
      public/app/core/services/bridge_srv.ts
  23. 3 4
      public/app/core/specs/bridge_srv.jest.ts
  24. 0 2
      public/app/core/utils/emitter.ts
  25. 0 2
      public/app/features/alerting/alert_def.ts
  26. 0 74
      public/app/features/alerting/alert_list_ctrl.ts
  27. 0 1
      public/app/features/alerting/all.ts
  28. 0 61
      public/app/features/alerting/partials/alert_list.html
  29. 3 0
      public/app/features/all.js
  30. 24 31
      public/app/features/dashboard/dashgrid/DashboardRow.tsx
  31. 0 8
      public/app/features/styleguide/styleguide.html
  32. 1 35
      public/app/features/styleguide/styleguide.ts
  33. 33 0
      public/app/routes/ReactContainer.tsx
  34. 1 1
      public/app/routes/dashboard_loaders.ts
  35. 15 63
      public/app/routes/routes.ts
  36. 34 0
      public/app/stores/AlertListStore/AlertListStore.ts
  37. 34 0
      public/app/stores/AlertListStore/AlertRule.ts
  38. 13 0
      public/app/stores/AlertListStore/helpers.ts
  39. 12 0
      public/app/stores/NavStore/NavItem.ts
  40. 41 0
      public/app/stores/NavStore/NavStore.ts
  41. 26 0
      public/app/stores/RootStore/RootStore.ts
  42. 10 0
      public/app/stores/SearchStore/ResultItem.ts
  43. 27 0
      public/app/stores/SearchStore/SearchResultSection.ts
  44. 22 0
      public/app/stores/SearchStore/SearchStore.ts
  45. 6 0
      public/app/stores/ServerStatsStore/ServerStat.ts
  46. 24 0
      public/app/stores/ServerStatsStore/ServerStatsStore.ts
  47. 46 0
      public/app/stores/ViewStore/ViewStore.ts
  48. 16 0
      public/app/stores/store.ts
  49. 5 5
      public/sass/components/_gf-form.scss
  50. 2 3
      public/sass/components/_page_header.scss
  51. 5 7
      public/sass/pages/_alerting.scss
  52. 15 0
      public/test/mocks/common.ts
  53. 1 1
      tsconfig.json
  54. 351 213
      yarn.lock

+ 2 - 1
jest.config.js

@@ -24,5 +24,6 @@ module.exports = {
   "setupFiles": [
     "./public/test/jest-shim.ts",
     "./public/test/jest-setup.ts"
-  ]
+  ],
+  "snapshotSerializers": ["enzyme-to-json/serializer"],
 };

+ 12 - 8
package.json

@@ -25,6 +25,7 @@
     "css-loader": "^0.28.7",
     "enzyme": "^3.1.0",
     "enzyme-adapter-react-16": "^1.0.1",
+    "enzyme-to-json": "^3.3.0",
     "es6-promise": "^3.0.2",
     "es6-shim": "^0.35.3",
     "expect.js": "~0.2.0",
@@ -54,7 +55,7 @@
     "html-loader": "^0.5.1",
     "html-webpack-plugin": "^2.30.1",
     "husky": "^0.14.3",
-    "jest": "^21.2.1",
+    "jest": "^22.0.4",
     "jshint-stylish": "~2.2.1",
     "json-loader": "^0.5.7",
     "karma": "1.7.0",
@@ -83,12 +84,12 @@
     "sinon": "1.17.6",
     "systemjs": "0.20.19",
     "systemjs-plugin-css": "^0.1.36",
-    "ts-jest": "^21.1.3",
-    "ts-loader": "^2.3.7",
-    "tslint": "^5.7.0",
+    "ts-jest": "^22.0.0",
+    "ts-loader": "^3.2.0",
+    "tslint": "^5.8.0",
     "tslint-loader": "^3.5.3",
-    "typescript": "^2.5.2",
-    "webpack": "^3.6.0",
+    "typescript": "^2.6.2",
+    "webpack": "^3.10.0",
     "webpack-bundle-analyzer": "^2.9.0",
     "webpack-cleanup-plugin": "^0.5.1",
     "webpack-merge": "^4.1.0",
@@ -138,12 +139,15 @@
     "file-saver": "^1.3.3",
     "jquery": "^3.2.1",
     "lodash": "^4.17.4",
+    "mobx": "^3.4.1",
+    "mobx-react": "^4.3.5",
+    "mobx-state-tree": "^1.3.1",
     "moment": "^2.18.1",
     "mousetrap": "^1.6.0",
     "perfect-scrollbar": "^1.2.0",
     "prop-types": "^15.6.0",
-    "react": "^16.1.1",
-    "react-dom": "^16.1.1",
+    "react": "^16.2.0",
+    "react-dom": "^16.2.0",
     "react-grid-layout": "^0.16.1",
     "react-sizeme": "^2.3.6",
     "remarkable": "^1.7.1",

+ 2 - 2
pkg/api/alerting.go

@@ -278,7 +278,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
 	}
 
 	var response models.AlertStateType = models.AlertStatePending
-	pausedState := "un paused"
+	pausedState := "un-paused"
 	if cmd.Paused {
 		response = models.AlertStatePaused
 		pausedState = "paused"
@@ -287,7 +287,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
 	result := map[string]interface{}{
 		"alertId": alertId,
 		"state":   response,
-		"message": "alert " + pausedState,
+		"message": "Alert " + pausedState,
 	}
 
 	return Json(200, result)

+ 1 - 1
pkg/services/sqlstore/alert.go

@@ -88,7 +88,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
 		params = append(params, query.PanelId)
 	}
 
-	if len(query.State) > 0 && query.State[0] != "ALL" {
+	if len(query.State) > 0 && query.State[0] != "all" {
 		sql.WriteString(` AND (`)
 		for i, v := range query.State {
 			if i > 0 {

+ 1 - 1
pkg/services/sqlstore/stats.go

@@ -13,7 +13,7 @@ func init() {
 	bus.AddHandler("sql", GetAdminStats)
 }
 
-var activeUserTimeLimit time.Duration = time.Hour * 24 * 14
+var activeUserTimeLimit time.Duration = time.Hour * 24 * 30
 
 func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
 	var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`

+ 3 - 0
public/app/app.ts

@@ -27,6 +27,7 @@ _.move = function(array, fromIndex, toIndex) {
 };
 
 import { coreModule, registerAngularDirectives } from './core/core';
+import { setupAngularRoutes } from './routes/routes';
 
 export class GrafanaApp {
   registerFunctions: any;
@@ -113,6 +114,7 @@ export class GrafanaApp {
     this.useModule(coreModule);
 
     // register react angular wrappers
+    coreModule.config(setupAngularRoutes);
     registerAngularDirectives();
 
     var preBootRequires = [System.import('app/features/all')];
@@ -121,6 +123,7 @@ export class GrafanaApp {
       .then(() => {
         // disable tool tip animation
         $.fn.tooltip.defaults.animation = false;
+
         // bootstrap the app
         angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
           _.each(this.preBootModules, module => {

+ 68 - 0
public/app/containers/AlertRuleList/AlertRuleList.jest.tsx

@@ -0,0 +1,68 @@
+import React from 'react';
+import moment from 'moment';
+import { AlertRuleList } from './AlertRuleList';
+import { RootStore } from 'app/stores/RootStore/RootStore';
+import { backendSrv, createNavTree } from 'test/mocks/common';
+import { mount } from 'enzyme';
+import toJson from 'enzyme-to-json';
+
+describe('AlertRuleList', () => {
+  let page, store;
+
+  beforeAll(() => {
+    backendSrv.get.mockReturnValue(
+      Promise.resolve([
+        {
+          id: 11,
+          dashboardId: 58,
+          panelId: 3,
+          name: 'Panel Title alert',
+          state: 'ok',
+          newStateDate: moment()
+            .subtract(5, 'minutes')
+            .format(),
+          evalData: {},
+          executionError: '',
+          dashboardUri: 'db/mygool',
+        },
+      ])
+    );
+
+    store = RootStore.create(
+      {},
+      {
+        backendSrv: backendSrv,
+        navTree: createNavTree('alerting', 'alert-list'),
+      }
+    );
+
+    page = mount(<AlertRuleList {...store} />);
+  });
+
+  it('should call api to get rules', () => {
+    expect(backendSrv.get.mock.calls[0][0]).toEqual('/api/alerts');
+  });
+
+  it('should render 1 rule', () => {
+    page.update();
+    let ruleNode = page.find('.card-item-wrapper');
+    expect(toJson(ruleNode)).toMatchSnapshot();
+  });
+
+  it('toggle state should change pause rule if not paused', async () => {
+    backendSrv.post.mockReturnValue(
+      Promise.resolve({
+        state: 'paused',
+      })
+    );
+
+    page.find('.fa-pause').simulate('click');
+
+    // wait for api call to resolve
+    await Promise.resolve();
+    page.update();
+
+    expect(store.alertList.rules[0].state).toBe('paused');
+    expect(page.find('.fa-play')).toHaveLength(1);
+  });
+});

+ 145 - 0
public/app/containers/AlertRuleList/AlertRuleList.tsx

@@ -0,0 +1,145 @@
+import React from 'react';
+import classNames from 'classnames';
+import { inject, observer } from 'mobx-react';
+import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import { IAlertRule } from 'app/stores/AlertListStore/AlertListStore';
+import appEvents from 'app/core/app_events';
+import IContainerProps from 'app/containers/IContainerProps';
+
+@inject('view', 'nav', 'alertList')
+@observer
+export class AlertRuleList extends React.Component<IContainerProps, any> {
+  stateFilters = [
+    { text: 'All', value: 'all' },
+    { text: 'OK', value: 'ok' },
+    { text: 'Not OK', value: 'not_ok' },
+    { text: 'Alerting', value: 'alerting' },
+    { text: 'No Data', value: 'no_data' },
+    { text: 'Paused', value: 'paused' },
+  ];
+
+  constructor(props) {
+    super(props);
+
+    this.props.nav.load('alerting', 'alert-list');
+    this.fetchRules();
+  }
+
+  onStateFilterChanged = evt => {
+    this.props.view.updateQuery({ state: evt.target.value });
+    this.fetchRules();
+  };
+
+  fetchRules() {
+    this.props.alertList.loadRules({
+      state: this.props.view.query.get('state') || 'all',
+    });
+  }
+
+  onOpenHowTo = () => {
+    appEvents.emit('show-modal', {
+      src: 'public/app/features/alerting/partials/alert_howto.html',
+      modalClass: 'confirm-modal',
+      model: {},
+    });
+  };
+
+  render() {
+    const { nav, alertList } = this.props;
+
+    return (
+      <div>
+        <PageHeader model={nav as any} />
+        <div className="page-container page-body">
+          <div className="page-action-bar">
+            <div className="gf-form">
+              <label className="gf-form-label">Filter by state</label>
+
+              <div className="gf-form-select-wrapper width-13">
+                <select className="gf-form-input" onChange={this.onStateFilterChanged} value={alertList.stateFilter}>
+                  {this.stateFilters.map(AlertStateFilterOption)}
+                </select>
+              </div>
+            </div>
+
+            <div className="page-action-bar__spacer" />
+
+            <a className="btn btn-secondary" onClick={this.onOpenHowTo}>
+              <i className="fa fa-info-circle" /> How to add an alert
+            </a>
+          </div>
+
+          <section className="card-section card-list-layout-list">
+            <ol className="card-list">{alertList.rules.map(rule => <AlertRuleItem rule={rule} key={rule.id} />)}</ol>
+          </section>
+        </div>
+      </div>
+    );
+  }
+}
+
+function AlertStateFilterOption({ text, value }) {
+  return (
+    <option key={value} value={value}>
+      {text}
+    </option>
+  );
+}
+
+export interface AlertRuleItemProps {
+  rule: IAlertRule;
+}
+
+@observer
+export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> {
+  toggleState = () => {
+    this.props.rule.togglePaused();
+  };
+
+  render() {
+    const { rule } = this.props;
+
+    let stateClass = classNames({
+      fa: true,
+      'fa-play': rule.isPaused,
+      'fa-pause': !rule.isPaused,
+    });
+
+    let ruleUrl = `dashboard/${rule.dashboardUri}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`;
+
+    return (
+      <li className="card-item-wrapper">
+        <div className="card-item card-item--alert">
+          <div className="card-item-header">
+            <div className="card-item-type">
+              <a
+                className="card-item-cog"
+                title="Pausing an alert rule prevents it from executing"
+                onClick={this.toggleState}
+              >
+                <i className={stateClass} />
+              </a>
+              <a className="card-item-cog" href={ruleUrl} title="Edit alert rule">
+                <i className="icon-gf icon-gf-settings" />
+              </a>
+            </div>
+          </div>
+          <div className="card-item-body">
+            <div className="card-item-details">
+              <div className="card-item-name">
+                <a href={ruleUrl}>{rule.name}</a>
+              </div>
+              <div className="card-item-sub-name">
+                <span className={`alert-list-item-state ${rule.stateClass}`}>
+                  <i className={rule.stateIcon} /> {rule.stateText}
+                </span>
+                <span> for {rule.stateAge}</span>
+              </div>
+              {rule.info && <div className="small muted">{rule.info}</div>}
+            </div>
+          </div>
+        </div>
+      </li>
+    );
+  }
+}

+ 72 - 0
public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap

@@ -0,0 +1,72 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AlertRuleList should render 1 rule 1`] = `
+<li
+  className="card-item-wrapper"
+>
+  <div
+    className="card-item card-item--alert"
+  >
+    <div
+      className="card-item-header"
+    >
+      <div
+        className="card-item-type"
+      >
+        <a
+          className="card-item-cog"
+          onClick={[Function]}
+          title="Pausing an alert rule prevents it from executing"
+        >
+          <i
+            className="fa fa-pause"
+          />
+        </a>
+        <a
+          className="card-item-cog"
+          href="dashboard/db/mygool?panelId=3&fullscreen&edit&tab=alert"
+          title="Edit alert rule"
+        >
+          <i
+            className="icon-gf icon-gf-settings"
+          />
+        </a>
+      </div>
+    </div>
+    <div
+      className="card-item-body"
+    >
+      <div
+        className="card-item-details"
+      >
+        <div
+          className="card-item-name"
+        >
+          <a
+            href="dashboard/db/mygool?panelId=3&fullscreen&edit&tab=alert"
+          >
+            Panel Title alert
+          </a>
+        </div>
+        <div
+          className="card-item-sub-name"
+        >
+          <span
+            className="alert-list-item-state alert-state-ok"
+          >
+            <i
+              className="icon-gf icon-gf-online"
+            />
+             
+            OK
+          </span>
+          <span>
+             for 
+            5 minutes
+          </span>
+        </div>
+      </div>
+    </div>
+  </div>
+</li>
+`;

+ 15 - 0
public/app/containers/IContainerProps.ts

@@ -0,0 +1,15 @@
+import { SearchStore } from './../stores/SearchStore/SearchStore';
+import { ServerStatsStore } from './../stores/ServerStatsStore/ServerStatsStore';
+import { NavStore } from './../stores/NavStore/NavStore';
+import { AlertListStore } from './../stores/AlertListStore/AlertListStore';
+import { ViewStore } from './../stores/ViewStore/ViewStore';
+
+interface IContainerProps {
+  search: typeof SearchStore.Type;
+  serverStats: typeof ServerStatsStore.Type;
+  nav: typeof NavStore.Type;
+  alertList: typeof AlertListStore.Type;
+  view: typeof ViewStore.Type;
+}
+
+export default IContainerProps;

+ 30 - 0
public/app/containers/ServerStats/ServerStats.jest.tsx

@@ -0,0 +1,30 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { ServerStats } from './ServerStats';
+import { RootStore } from 'app/stores/RootStore/RootStore';
+import { backendSrv, createNavTree } from 'test/mocks/common';
+
+describe('ServerStats', () => {
+  it('Should render table with stats', done => {
+    backendSrv.get.mockReturnValue(
+      Promise.resolve({
+        dashboards: 10,
+      })
+    );
+
+    const store = RootStore.create(
+      {},
+      {
+        backendSrv: backendSrv,
+        navTree: createNavTree('cfg', 'admin', 'server-stats'),
+      }
+    );
+
+    const page = renderer.create(<ServerStats {...store} />);
+
+    setTimeout(() => {
+      expect(page.toJSON()).toMatchSnapshot();
+      done();
+    });
+  });
+});

+ 45 - 0
public/app/containers/ServerStats/ServerStats.tsx

@@ -0,0 +1,45 @@
+import React from 'react';
+import { inject, observer } from 'mobx-react';
+import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import IContainerProps from 'app/containers/IContainerProps';
+
+@inject('nav', 'serverStats')
+@observer
+export class ServerStats extends React.Component<IContainerProps, any> {
+  constructor(props) {
+    super(props);
+    const { nav, serverStats } = this.props;
+
+    nav.load('cfg', 'admin', 'server-stats');
+    serverStats.load();
+  }
+
+  render() {
+    const { nav, serverStats } = this.props;
+    return (
+      <div>
+        <PageHeader model={nav as any} />
+        <div className="page-container page-body">
+          <table className="filter-table form-inline">
+            <thead>
+              <tr>
+                <th>Name</th>
+                <th>Value</th>
+              </tr>
+            </thead>
+            <tbody>{serverStats.stats.map(StatItem)}</tbody>
+          </table>
+        </div>
+      </div>
+    );
+  }
+}
+
+function StatItem(stat) {
+  return (
+    <tr key={stat.name}>
+      <td>{stat.name}</td>
+      <td>{stat.value}</td>
+    </tr>
+  );
+}

+ 170 - 0
public/app/containers/ServerStats/__snapshots__/ServerStats.jest.tsx.snap

@@ -0,0 +1,170 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ServerStats Should render table with stats 1`] = `
+<div>
+  <div
+    className="page-header-canvas"
+  >
+    <div
+      className="page-container"
+    >
+      <div
+        className="page-header"
+      >
+        <div
+          className="page-header__inner"
+        >
+          <span
+            className="page-header__logo"
+          >
+            
+            
+          </span>
+          <div
+            className="page-header__info-block"
+          >
+            <h1
+              className="page-header__title"
+            >
+              admin-Text
+            </h1>
+            
+          </div>
+        </div>
+        <nav>
+          <div
+            className="gf-form-select-wrapper width-20 page-header__select-nav"
+          >
+            <label
+              className="gf-form-select-icon "
+              htmlFor="page-header-select-nav"
+            />
+            <select
+              className="gf-select-nav gf-form-input"
+              defaultValue="/url/server-stats"
+              id="page-header-select-nav"
+              onChange={[Function]}
+            >
+              <option
+                value="/url/server-stats"
+              >
+                server-stats-Text
+              </option>
+            </select>
+          </div>
+          <ul
+            className="gf-tabs page-header__tabs"
+          >
+            <li
+              className="gf-tabs-item"
+            >
+              <a
+                className="gf-tabs-link active"
+                href="/url/server-stats"
+                target={undefined}
+              >
+                <i
+                  className=""
+                />
+                server-stats-Text
+              </a>
+            </li>
+          </ul>
+        </nav>
+      </div>
+    </div>
+  </div>
+  <div
+    className="page-container page-body"
+  >
+    <table
+      className="filter-table form-inline"
+    >
+      <thead>
+        <tr>
+          <th>
+            Name
+          </th>
+          <th>
+            Value
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td>
+            Total dashboards
+          </td>
+          <td>
+            10
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Total users
+          </td>
+          <td>
+            0
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Active users (seen last 30 days)
+          </td>
+          <td>
+            0
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Total orgs
+          </td>
+          <td>
+            0
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Total playlists
+          </td>
+          <td>
+            0
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Total snapshots
+          </td>
+          <td>
+            0
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Total dashboard tags
+          </td>
+          <td>
+            0
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Total starred dashboards
+          </td>
+          <td>
+            0
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Total alerts
+          </td>
+          <td>
+            0
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</div>
+`;

+ 11 - 9
public/app/core/angular_wrappers.ts

@@ -1,12 +1,14 @@
-import { react2AngularDirective } from 'app/core/utils/react2angular';
-import { PasswordStrength } from './components/PasswordStrength';
-import PageHeader from './components/PageHeader/PageHeader';
-import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
-import LoginBackground from './components/Login/LoginBackground';
+import { react2AngularDirective } from "app/core/utils/react2angular";
+import { PasswordStrength } from "./components/PasswordStrength";
+import PageHeader from "./components/PageHeader/PageHeader";
+import EmptyListCTA from "./components/EmptyListCTA/EmptyListCTA";
+import LoginBackground from "./components/Login/LoginBackground";
+import { SearchResult } from "./components/search/SearchResult";
 
 export function registerAngularDirectives() {
-  react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
-  react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
-  react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
-  react2AngularDirective('loginBackground', LoginBackground, []);
+  react2AngularDirective("passwordStrength", PasswordStrength, ["password"]);
+  react2AngularDirective("pageHeader", PageHeader, ["model", "noTabs"]);
+  react2AngularDirective("emptyListCta", EmptyListCTA, ["model"]);
+  react2AngularDirective("loginBackground", LoginBackground, []);
+  react2AngularDirective("searchResult", SearchResult, []);
 }

+ 12 - 27
public/app/core/components/PageHeader/PageHeader.tsx

@@ -1,7 +1,7 @@
-import React from "react";
-import { NavModel, NavModelItem } from "../../nav_model_srv";
-import classNames from "classnames";
-import appEvents from "app/core/app_events";
+import React from 'react';
+import { NavModel, NavModelItem } from '../../nav_model_srv';
+import classNames from 'classnames';
+import appEvents from 'app/core/app_events';
 
 export interface IProps {
   model: NavModel;
@@ -13,8 +13,8 @@ function TabItem(tab: NavModelItem) {
   }
 
   let tabClasses = classNames({
-    "gf-tabs-link": true,
-    active: tab.active
+    'gf-tabs-link': true,
+    active: tab.active,
   });
 
   return (
@@ -49,13 +49,7 @@ function Navigation({ main }: { main: NavModelItem }) {
   );
 }
 
-function SelectNav({
-  main,
-  customCss
-}: {
-  main: NavModelItem;
-  customCss: string;
-}) {
+function SelectNav({ main, customCss }: { main: NavModelItem; customCss: string }) {
   const defaultSelectedItem = main.children.find(navItem => {
     return navItem.active === true;
   });
@@ -63,15 +57,12 @@ function SelectNav({
   const gotoUrl = evt => {
     var element = evt.target;
     var url = element.options[element.selectedIndex].value;
-    appEvents.emit("location-change", { href: url });
+    appEvents.emit('location-change', { href: url });
   };
 
   return (
     <div className={`gf-form-select-wrapper width-20 ${customCss}`}>
-      <label
-        className={`gf-form-select-icon ${defaultSelectedItem.icon}`}
-        htmlFor="page-header-select-nav"
-      />
+      <label className={`gf-form-select-icon ${defaultSelectedItem.icon}`} htmlFor="page-header-select-nav" />
       {/* Label to make it clickable */}
       <select
         className="gf-select-nav gf-form-input"
@@ -86,9 +77,7 @@ function SelectNav({
 }
 
 function Tabs({ main, customCss }: { main: NavModelItem; customCss: string }) {
-  return (
-    <ul className={`gf-tabs ${customCss}`}>{main.children.map(TabItem)}</ul>
-  );
+  return <ul className={`gf-tabs ${customCss}`}>{main.children.map(TabItem)}</ul>;
 }
 
 export default class PageHeader extends React.Component<IProps, any> {
@@ -125,13 +114,9 @@ export default class PageHeader extends React.Component<IProps, any> {
           {main.text && <h1 className="page-header__title">{main.text}</h1>}
           {main.breadcrumbs &&
             main.breadcrumbs.length > 0 && (
-              <h1 className="page-header__title">
-                {this.renderBreadcrumb(main.breadcrumbs)}
-              </h1>
+              <h1 className="page-header__title">{this.renderBreadcrumb(main.breadcrumbs)}</h1>
             )}
-          {main.subTitle && (
-            <div className="page-header__sub-title">{main.subTitle}</div>
-          )}
+          {main.subTitle && <div className="page-header__sub-title">{main.subTitle}</div>}
           {main.subType && (
             <div className="page-header__stamps">
               <i className={main.subType.icon} />

+ 7 - 64
public/app/core/components/grafana_app.ts

@@ -6,20 +6,22 @@ import coreModule from 'app/core/core_module';
 import { profiler } from 'app/core/profiler';
 import appEvents from 'app/core/app_events';
 import Drop from 'tether-drop';
+import { createStore } from 'app/stores/store';
 
 export class GrafanaCtrl {
   /** @ngInject */
-  constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, globalEventSrv) {
+  constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, bridgeSrv, backendSrv) {
+    createStore(backendSrv);
+
     $scope.init = function() {
       $scope.contextSrv = contextSrv;
-
-      $rootScope.appSubUrl = config.appSubUrl;
+      $scope.appSubUrl = config.appSubUrl;
       $scope._ = _;
 
       profiler.init(config, $rootScope);
       alertSrv.init();
       utilSrv.init();
-      globalEventSrv.init();
+      bridgeSrv.init();
 
       $scope.dashAlerts = alertSrv;
     };
@@ -46,71 +48,12 @@ export class GrafanaCtrl {
       appEvents.emit(name, payload);
     };
 
-    $rootScope.colors = [
-      '#7EB26D',
-      '#EAB839',
-      '#6ED0E0',
-      '#EF843C',
-      '#E24D42',
-      '#1F78C1',
-      '#BA43A9',
-      '#705DA0',
-      '#508642',
-      '#CCA300',
-      '#447EBC',
-      '#C15C17',
-      '#890F02',
-      '#0A437C',
-      '#6D1F62',
-      '#584477',
-      '#B7DBAB',
-      '#F4D598',
-      '#70DBED',
-      '#F9BA8F',
-      '#F29191',
-      '#82B5D8',
-      '#E5A8E2',
-      '#AEA2E0',
-      '#629E51',
-      '#E5AC0E',
-      '#64B0C8',
-      '#E0752D',
-      '#BF1B00',
-      '#0A50A1',
-      '#962D82',
-      '#614D93',
-      '#9AC48A',
-      '#F2C96D',
-      '#65C5DB',
-      '#F9934E',
-      '#EA6460',
-      '#5195CE',
-      '#D683CE',
-      '#806EB7',
-      '#3F6833',
-      '#967302',
-      '#2F575E',
-      '#99440A',
-      '#58140C',
-      '#052B51',
-      '#511749',
-      '#3F2B5B',
-      '#E0F9D7',
-      '#FCEACA',
-      '#CFFAFF',
-      '#F9E2D2',
-      '#FCE2DE',
-      '#BADFF4',
-      '#F9D9F9',
-      '#DEDAF7',
-    ];
-
     $scope.init();
   }
 }
 
 /** @ngInject */
-export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope) {
+export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope, $location) {
   return {
     restrict: 'E',
     controller: GrafanaCtrl,

+ 86 - 0
public/app/core/components/search/SearchResult.tsx

@@ -0,0 +1,86 @@
+import React from "react";
+import classNames from "classnames";
+import { observer } from "mobx-react";
+import { store } from "app/stores/store";
+
+export interface SearchResultProps {
+  search: any;
+}
+
+@observer
+export class SearchResult extends React.Component<SearchResultProps, any> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      search: store.search
+    };
+
+    store.search.query();
+  }
+
+  render() {
+    return this.state.search.sections.map(section => {
+      return <SearchResultSection section={section} key={section.id} />;
+    });
+  }
+}
+
+export interface SectionProps {
+  section: any;
+}
+
+@observer
+export class SearchResultSection extends React.Component<SectionProps, any> {
+  constructor(props) {
+    super(props);
+  }
+
+  renderItem(item) {
+    return (
+      <a className="search-item" href={item.url} key={item.id}>
+        <span className="search-item__icon">
+          <i className="fa fa-th-large" />
+        </span>
+        <span className="search-item__body">
+          <div className="search-item__body-title">{item.title}</div>
+        </span>
+      </a>
+    );
+  }
+
+  toggleSection = () => {
+    this.props.section.toggle();
+  };
+
+  render() {
+    let collapseClassNames = classNames({
+      fa: true,
+      "fa-plus": !this.props.section.expanded,
+      "fa-minus": this.props.section.expanded,
+      "search-section__header__toggle": true
+    });
+
+    return (
+      <div className="search-section" key={this.props.section.id}>
+        <div className="search-section__header">
+          <i
+            className={classNames(
+              "search-section__header__icon",
+              this.props.section.icon
+            )}
+          />
+          <span className="search-section__header__text">
+            {this.props.section.title}
+          </span>
+          <i className={collapseClassNames} onClick={this.toggleSection} />
+        </div>
+        {this.props.section.expanded && (
+          <div className="search-section__items">
+            {this.props.section.items.map(this.renderItem)}
+          </div>
+        )}
+      </div>
+    );
+  }
+}

+ 4 - 2
public/app/core/components/sidemenu/sidemenu.ts

@@ -16,8 +16,10 @@ export class SideMenuCtrl {
   constructor(private $scope, private $rootScope, private $location, private contextSrv, private $timeout) {
     this.isSignedIn = contextSrv.isSignedIn;
     this.user = contextSrv.user;
-    this.mainLinks = _.filter(config.bootData.navTree, item => !item.hideFromMenu);
-    this.bottomNav = _.filter(config.bootData.navTree, item => item.hideFromMenu);
+
+    let navTree = _.cloneDeep(config.bootData.navTree);
+    this.mainLinks = _.filter(navTree, item => !item.hideFromMenu);
+    this.bottomNav = _.filter(navTree, item => item.hideFromMenu);
     this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
 
     if (contextSrv.user.orgCount > 1) {

+ 0 - 1
public/app/core/core.ts

@@ -35,7 +35,6 @@ import { queryPartEditorDirective } from './components/query_part/query_part_edi
 import { formDropdownDirective } from './components/form_dropdown/form_dropdown';
 import 'app/core/controllers/all';
 import 'app/core/services/all';
-import 'app/core/routes/routes';
 import './filters/filters';
 import coreModule from './core_module';
 import appEvents from './app_events';

+ 0 - 26
public/app/core/routes/bundle_loader.ts

@@ -1,26 +0,0 @@
-export class BundleLoader {
-  lazy: any;
-
-  constructor(bundleName) {
-    var defer = null;
-
-    this.lazy = [
-      '$q',
-      '$route',
-      '$rootScope',
-      ($q, $route, $rootScope) => {
-        if (defer) {
-          return defer.promise;
-        }
-
-        defer = $q.defer();
-
-        System.import(bundleName).then(() => {
-          defer.resolve();
-        });
-
-        return defer.promise;
-      },
-    ];
-  }
-}

+ 1 - 1
public/app/core/services/all.js

@@ -8,6 +8,6 @@ define([
   './segment_srv',
   './backend_srv',
   './dynamic_directive_srv',
-  './global_event_srv'
+  './bridge_srv'
 ],
 function () {});

+ 32 - 6
public/app/core/services/global_event_srv.ts → public/app/core/services/bridge_srv.ts

@@ -1,15 +1,16 @@
-import coreModule from 'app/core/core_module';
+import coreModule from 'app/core/core_module';
 import config from 'app/core/config';
 import appEvents from 'app/core/app_events';
+import { store } from 'app/stores/store';
+import { reaction } from 'mobx';
 
-// This service is for registering global events.
-// Good for communication react > angular and vice verse
-export class GlobalEventSrv {
+// Services that handles angular -> mobx store sync & other react <-> angular sync
+export class BridgeSrv {
   private appSubUrl;
   private fullPageReloadRoutes;
 
   /** @ngInject */
-  constructor(private $location, private $timeout, private $window) {
+  constructor(private $location, private $timeout, private $window, private $rootScope) {
     this.appSubUrl = config.appSubUrl;
     this.fullPageReloadRoutes = ['/logout'];
   }
@@ -25,6 +26,31 @@ export class GlobalEventSrv {
   }
 
   init() {
+    this.$rootScope.$on('$routeUpdate', (evt, data) => {
+      let angularUrl = this.$location.url();
+      if (store.view.currentUrl !== angularUrl) {
+        store.view.updatePathAndQuery(this.$location.path(), this.$location.search());
+      }
+    });
+
+    this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
+      let angularUrl = this.$location.url();
+      if (store.view.currentUrl !== angularUrl) {
+        store.view.updatePathAndQuery(this.$location.path(), this.$location.search());
+      }
+    });
+
+    reaction(
+      () => store.view.currentUrl,
+      currentUrl => {
+        let angularUrl = this.$location.url();
+        if (angularUrl !== currentUrl) {
+          this.$location.url(currentUrl);
+          console.log('store updating angular $location.url', currentUrl);
+        }
+      }
+    );
+
     appEvents.on('location-change', payload => {
       const urlWithoutBase = this.stripBaseFromUrl(payload.href);
       if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) {
@@ -40,4 +66,4 @@ export class GlobalEventSrv {
   }
 }
 
-coreModule.service('globalEventSrv', GlobalEventSrv);
+coreModule.service('bridgeSrv', BridgeSrv);

+ 3 - 4
public/app/core/specs/global_event_srv.jest.ts → public/app/core/specs/bridge_srv.jest.ts

@@ -1,5 +1,4 @@
-import { GlobalEventSrv } from 'app/core/services/global_event_srv';
-import { beforeEach } from 'test/lib/common';
+import { BridgeSrv } from 'app/core/services/bridge_srv';
 
 jest.mock('app/core/config', () => {
   return {
@@ -7,11 +6,11 @@ jest.mock('app/core/config', () => {
   };
 });
 
-describe('GlobalEventSrv', () => {
+describe('BridgeSrv', () => {
   let searchSrv;
 
   beforeEach(() => {
-    searchSrv = new GlobalEventSrv(null, null, null);
+    searchSrv = new BridgeSrv(null, null, null, null);
   });
 
   describe('With /subUrl as appSubUrl', () => {

+ 0 - 2
public/app/core/utils/emitter.ts

@@ -1,5 +1,3 @@
-///<reference path="../../headers/common.d.ts" />
-
 import EventEmitter from 'eventemitter3';
 
 export class Emitter {

+ 0 - 2
public/app/features/alerting/alert_def.ts

@@ -1,5 +1,3 @@
-///<reference path="../../headers/common.d.ts" />
-
 import _ from 'lodash';
 import { QueryPartDef, QueryPart } from 'app/core/components/query_part/query_part';
 

+ 0 - 74
public/app/features/alerting/alert_list_ctrl.ts

@@ -1,74 +0,0 @@
-///<reference path="../../headers/common.d.ts" />
-
-import _ from 'lodash';
-import moment from 'moment';
-
-import { coreModule, appEvents } from 'app/core/core';
-import alertDef from './alert_def';
-
-export class AlertListCtrl {
-  alerts: any;
-  stateFilters = [
-    { text: 'All', value: null },
-    { text: 'OK', value: 'ok' },
-    { text: 'Not OK', value: 'not_ok' },
-    { text: 'Alerting', value: 'alerting' },
-    { text: 'No Data', value: 'no_data' },
-    { text: 'Paused', value: 'paused' },
-  ];
-  filters = {
-    state: 'ALL',
-  };
-  navModel: any;
-
-  /** @ngInject */
-  constructor(private backendSrv, private $location, navModelSrv) {
-    this.navModel = navModelSrv.getNav('alerting', 'alert-list', 0);
-
-    var params = $location.search();
-    this.filters.state = params.state || null;
-    this.loadAlerts();
-  }
-
-  filtersChanged() {
-    this.$location.search(this.filters);
-  }
-
-  loadAlerts() {
-    this.backendSrv.get('/api/alerts', this.filters).then(result => {
-      this.alerts = _.map(result, alert => {
-        alert.stateModel = alertDef.getStateDisplayModel(alert.state);
-        alert.newStateDateAgo = moment(alert.newStateDate)
-          .fromNow()
-          .replace(' ago', '');
-        if (alert.evalData && alert.evalData.no_data) {
-          alert.no_data = true;
-        }
-        return alert;
-      });
-    });
-  }
-
-  pauseAlertRule(alertId: any) {
-    var alert = _.find(this.alerts, { id: alertId });
-
-    var payload = {
-      paused: alert.state !== 'paused',
-    };
-
-    this.backendSrv.post(`/api/alerts/${alert.id}/pause`, payload).then(result => {
-      alert.state = result.state;
-      alert.stateModel = alertDef.getStateDisplayModel(result.state);
-    });
-  }
-
-  openHowTo() {
-    appEvents.emit('show-modal', {
-      src: 'public/app/features/alerting/partials/alert_howto.html',
-      modalClass: 'confirm-modal',
-      model: {},
-    });
-  }
-}
-
-coreModule.controller('AlertListCtrl', AlertListCtrl);

+ 0 - 1
public/app/features/alerting/all.ts

@@ -1,3 +1,2 @@
-import './alert_list_ctrl';
 import './notifications_list_ctrl';
 import './notification_edit_ctrl';

+ 0 - 61
public/app/features/alerting/partials/alert_list.html

@@ -1,61 +0,0 @@
-<page-header model="ctrl.navModel"></page-header>
-
-<div class="page-container page-body">
-
-  <div class="page-action-bar">
-    <div class="gf-form">
-      <label class="gf-form-label">Filter by state</label>
-      <div class="gf-form-select-wrapper width-13">
-        <select class="gf-form-input" ng-model="ctrl.filters.state" ng-options="f.value as f.text for f in ctrl.stateFilters" ng-change="ctrl.filtersChanged()">
-        </select>
-      </div>
-    </div>
-
-    <div class="page-action-bar__spacer">
-    </div>
-
-    <a class="btn btn-secondary" ng-click="ctrl.openHowTo()">
-      <i class="fa fa-info-circle"></i>
-      How to add an alert
-    </a>
-  </div>
-
-  <section class="card-section card-list-layout-list">
-
-    <ol class="card-list" >
-      <li class="card-item-wrapper" ng-repeat="alert in ctrl.alerts">
-        <div class="card-item card-item--alert">
-          <div class="card-item-header">
-            <div class="card-item-type">
-              <a class="card-item-cog" bs-tooltip="'Pausing an alert rule prevents it from executing'" ng-click="ctrl.pauseAlertRule(alert.id)">
-                <i ng-show="alert.state !== 'paused'" class="fa fa-pause"></i>
-                <i ng-show="alert.state === 'paused'" class="fa fa-play"></i>
-              </a>
-              <a class="card-item-cog" href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert" bs-tooltip="'Edit alert rule'">
-                <i class="icon-gf icon-gf-settings"></i>
-              </a>
-            </div>
-          </div>
-          <div class="card-item-body">
-            <div class="card-item-details">
-              <div class="card-item-name">
-                <a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert">
-                  {{alert.name}}
-                </a>
-              </div>
-              <div class="card-item-sub-name">
-                <span class="alert-list-item-state {{alert.stateModel.stateClass}}">
-                  <i class="{{alert.stateModel.iconClass}}"></i>
-                  {{alert.stateModel.text}} <span class="small muted" ng-show="alert.no_data">(due to no data)</span>
-                </span> for {{alert.newStateDateAgo}}
-              </div>
-              <div class="small muted" ng-show="alert.executionError !== ''">
-                Error: "{{alert.executionError}}"
-              </div>
-            </div>
-          </div>
-        </div>
-      </li>
-    </ol>
-  </section>
-</div>

+ 3 - 0
public/app/features/all.js

@@ -8,5 +8,8 @@ define([
   './playlist/all',
   './snapshot/all',
   './panel/all',
+  './org/all',
+  './admin/admin',
+  './alerting/all',
   './styleguide/styleguide',
 ], function () {});

+ 24 - 31
public/app/features/dashboard/dashgrid/DashboardRow.tsx

@@ -1,9 +1,9 @@
-import React from "react";
-import classNames from "classnames";
-import { PanelModel } from "../panel_model";
-import { PanelContainer } from "./PanelContainer";
-import templateSrv from "app/features/templating/template_srv";
-import appEvents from "app/core/app_events";
+import React from 'react';
+import classNames from 'classnames';
+import { PanelModel } from '../panel_model';
+import { PanelContainer } from './PanelContainer';
+import templateSrv from 'app/features/templating/template_srv';
+import appEvents from 'app/core/app_events';
 
 export interface DashboardRowProps {
   panel: PanelModel;
@@ -18,7 +18,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
     super(props);
 
     this.state = {
-      collapsed: this.props.panel.collapsed
+      collapsed: this.props.panel.collapsed,
     };
 
     this.panelContainer = this.props.getPanelContainer();
@@ -38,22 +38,22 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
   }
 
   openSettings() {
-    appEvents.emit("show-modal", {
+    appEvents.emit('show-modal', {
       templateHtml: `<row-options row="model.row" on-updated="model.onUpdated()" dismiss="dismiss()"></row-options>`,
-      modalClass: "modal--narrow",
+      modalClass: 'modal--narrow',
       model: {
         row: this.props.panel,
-        onUpdated: this.forceUpdate.bind(this)
-      }
+        onUpdated: this.forceUpdate.bind(this),
+      },
     });
   }
 
   delete() {
-    appEvents.emit("confirm-modal", {
-      title: "Delete Row",
-      text: "Are you sure you want to remove this row and all its panels?",
-      altActionText: "Delete row only",
-      icon: "fa-trash",
+    appEvents.emit('confirm-modal', {
+      title: 'Delete Row',
+      text: 'Are you sure you want to remove this row and all its panels?',
+      altActionText: 'Delete row only',
+      icon: 'fa-trash',
       onConfirm: () => {
         const panelContainer = this.props.getPanelContainer();
         const dashboard = panelContainer.getDashboard();
@@ -63,37 +63,30 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
         const panelContainer = this.props.getPanelContainer();
         const dashboard = panelContainer.getDashboard();
         dashboard.removeRow(this.props.panel, false);
-      }
+      },
     });
   }
 
   render() {
     const classes = classNames({
-      "dashboard-row": true,
-      "dashboard-row--collapsed": this.state.collapsed
+      'dashboard-row': true,
+      'dashboard-row--collapsed': this.state.collapsed,
     });
     const chevronClass = classNames({
       fa: true,
-      "fa-chevron-down": !this.state.collapsed,
-      "fa-chevron-right": this.state.collapsed
+      'fa-chevron-down': !this.state.collapsed,
+      'fa-chevron-right': this.state.collapsed,
     });
 
-    let title = templateSrv.replaceWithText(
-      this.props.panel.title,
-      this.props.panel.scopedVars
-    );
-    const hiddenPanels = this.props.panel.panels
-      ? this.props.panel.panels.length
-      : 0;
+    let title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
+    const hiddenPanels = this.props.panel.panels ? this.props.panel.panels.length : 0;
 
     return (
       <div className={classes}>
         <a className="dashboard-row__title pointer" onClick={this.toggle}>
           <i className={chevronClass} />
           {title}
-          <span className="dashboard-row__panel_count">
-            ({hiddenPanels} hidden panels)
-          </span>
+          <span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span>
         </a>
         <div className="dashboard-row__actions">
           <a className="pointer" onClick={this.openSettings}>

+ 0 - 8
public/app/features/styleguide/styleguide.html

@@ -14,14 +14,6 @@
 		</div>
 	</div>
 
-	<div class="tab-pane style-guide-icon-list">
-		<div class="row">
-			<div ng-repeat="icon in ctrl.icons" class="col-md-2 col-sm-3 col-xs-4">
-				<i class="icon-gf icon-gf-{{icon}}" bs-tooltip="'icon-gf icon-gf-{{icon}}'"></i>
-			</div>
-		</div>
-	</div>
-
 	<h3 class="page-heading">Forms</h3>
 
 	<div class="gf-form-inline">

+ 1 - 35
public/app/features/styleguide/styleguide.ts

@@ -1,51 +1,17 @@
 import coreModule from 'app/core/core_module';
 import config from 'app/core/config';
-import _ from 'lodash';
 
 class StyleGuideCtrl {
-  colors: any = [];
   theme: string;
   buttonNames = ['primary', 'secondary', 'inverse', 'success', 'warning', 'danger'];
   buttonSizes = ['btn-small', '', 'btn-large'];
   buttonVariants = ['-'];
-  icons: any = [];
-  page: any;
-  pages = ['colors', 'buttons', 'icons', 'plugins'];
   navModel: any;
 
   /** @ngInject **/
-  constructor(private $http, private $routeParams, private backendSrv, navModelSrv) {
+  constructor(private $routeParams, private backendSrv, navModelSrv) {
     this.navModel = navModelSrv.getNav('cfg', 'admin', 'styleguide', 1);
     this.theme = config.bootData.user.lightTheme ? 'light' : 'dark';
-    this.page = {};
-
-    if ($routeParams.page) {
-      this.page[$routeParams.page] = 1;
-    } else {
-      this.page.colors = true;
-    }
-
-    if (this.page.colors) {
-      this.loadColors();
-    }
-
-    if (this.page.icons) {
-      this.loadIcons();
-    }
-  }
-
-  loadColors() {
-    this.$http.get('public/build/styleguide.json').then(res => {
-      this.colors = _.map(res.data[this.theme], (value, key) => {
-        return { name: key, value: value };
-      });
-    });
-  }
-
-  loadIcons() {
-    this.$http.get('public/sass/icons.json').then(res => {
-      this.icons = res.data;
-    });
   }
 
   switchTheme() {

+ 33 - 0
public/app/routes/ReactContainer.tsx

@@ -0,0 +1,33 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import coreModule from 'app/core/core_module';
+import { store } from 'app/stores/store';
+import { Provider } from 'mobx-react';
+
+function WrapInProvider(store, Component, props) {
+  return (
+    <Provider {...store}>
+      <Component {...props} />
+    </Provider>
+  );
+}
+
+/** @ngInject */
+export function reactContainer($route, $location) {
+  return {
+    restrict: 'E',
+    template: '',
+    link(scope, elem) {
+      let component = $route.current.locals.component;
+      let props = {};
+
+      ReactDOM.render(WrapInProvider(store, component, props), elem[0]);
+
+      scope.$on('$destroy', function() {
+        ReactDOM.unmountComponentAtNode(elem[0]);
+      });
+    },
+  };
+}
+
+coreModule.directive('reactContainer', reactContainer);

+ 1 - 1
public/app/core/routes/dashboard_loaders.ts → public/app/routes/dashboard_loaders.ts

@@ -1,4 +1,4 @@
-import coreModule from '../core_module';
+import coreModule from 'app/core/core_module';
 
 export class LoadDashboardCtrl {
   /** @ngInject */

+ 15 - 63
public/app/core/routes/routes.ts → public/app/routes/routes.ts

@@ -1,43 +1,12 @@
 import './dashboard_loaders';
-import coreModule from 'app/core/core_module';
+import './ReactContainer';
+import { ServerStats } from 'app/containers/ServerStats/ServerStats';
+import { AlertRuleList } from 'app/containers/AlertRuleList/AlertRuleList';
 
 /** @ngInject **/
-function setupAngularRoutes($routeProvider, $locationProvider) {
+export function setupAngularRoutes($routeProvider, $locationProvider) {
   $locationProvider.html5Mode(true);
 
-  var loadOrgBundle = {
-    lazy: [
-      '$q',
-      '$route',
-      '$rootScope',
-      ($q, $route, $rootScope) => {
-        return System.import('app/features/org/all');
-      },
-    ],
-  };
-
-  var loadAdminBundle = {
-    lazy: [
-      '$q',
-      '$route',
-      '$rootScope',
-      ($q, $route, $rootScope) => {
-        return System.import('app/features/admin/admin');
-      },
-    ],
-  };
-
-  var loadAlertingBundle = {
-    lazy: [
-      '$q',
-      '$route',
-      '$rootScope',
-      ($q, $route, $rootScope) => {
-        return System.import('app/features/alerting/all');
-      },
-    ],
-  };
-
   $routeProvider
     .when('/', {
       templateUrl: 'public/app/partials/dashboard.html',
@@ -111,110 +80,92 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
     .when('/org', {
       templateUrl: 'public/app/features/org/partials/orgDetails.html',
       controller: 'OrgDetailsCtrl',
-      resolve: loadOrgBundle,
     })
     .when('/org/new', {
       templateUrl: 'public/app/features/org/partials/newOrg.html',
       controller: 'NewOrgCtrl',
-      resolve: loadOrgBundle,
     })
     .when('/org/users', {
       templateUrl: 'public/app/features/org/partials/orgUsers.html',
       controller: 'OrgUsersCtrl',
       controllerAs: 'ctrl',
-      resolve: loadOrgBundle,
     })
     .when('/org/users/invite', {
       templateUrl: 'public/app/features/org/partials/invite.html',
       controller: 'UserInviteCtrl',
       controllerAs: 'ctrl',
-      resolve: loadOrgBundle,
     })
     .when('/org/apikeys', {
       templateUrl: 'public/app/features/org/partials/orgApiKeys.html',
       controller: 'OrgApiKeysCtrl',
-      resolve: loadOrgBundle,
     })
     .when('/org/teams', {
       templateUrl: 'public/app/features/org/partials/teams.html',
       controller: 'TeamsCtrl',
       controllerAs: 'ctrl',
-      resolve: loadOrgBundle,
     })
     .when('/org/teams/new', {
       templateUrl: 'public/app/features/org/partials/create_team.html',
       controller: 'CreateTeamCtrl',
       controllerAs: 'ctrl',
-      resolve: loadOrgBundle,
     })
     .when('/org/teams/edit/:id', {
       templateUrl: 'public/app/features/org/partials/team_details.html',
       controller: 'TeamDetailsCtrl',
       controllerAs: 'ctrl',
-      resolve: loadOrgBundle,
     })
     .when('/profile', {
       templateUrl: 'public/app/features/org/partials/profile.html',
       controller: 'ProfileCtrl',
       controllerAs: 'ctrl',
-      resolve: loadOrgBundle,
     })
     .when('/profile/password', {
       templateUrl: 'public/app/features/org/partials/change_password.html',
       controller: 'ChangePasswordCtrl',
-      resolve: loadOrgBundle,
     })
     .when('/profile/select-org', {
       templateUrl: 'public/app/features/org/partials/select_org.html',
       controller: 'SelectOrgCtrl',
-      resolve: loadOrgBundle,
     })
     // ADMIN
     .when('/admin', {
       templateUrl: 'public/app/features/admin/partials/admin_home.html',
       controller: 'AdminHomeCtrl',
       controllerAs: 'ctrl',
-      resolve: loadAdminBundle,
     })
     .when('/admin/settings', {
       templateUrl: 'public/app/features/admin/partials/settings.html',
       controller: 'AdminSettingsCtrl',
       controllerAs: 'ctrl',
-      resolve: loadAdminBundle,
     })
     .when('/admin/users', {
       templateUrl: 'public/app/features/admin/partials/users.html',
       controller: 'AdminListUsersCtrl',
       controllerAs: 'ctrl',
-      resolve: loadAdminBundle,
     })
     .when('/admin/users/create', {
       templateUrl: 'public/app/features/admin/partials/new_user.html',
       controller: 'AdminEditUserCtrl',
-      resolve: loadAdminBundle,
     })
     .when('/admin/users/edit/:id', {
       templateUrl: 'public/app/features/admin/partials/edit_user.html',
       controller: 'AdminEditUserCtrl',
-      resolve: loadAdminBundle,
     })
     .when('/admin/orgs', {
       templateUrl: 'public/app/features/admin/partials/orgs.html',
       controller: 'AdminListOrgsCtrl',
       controllerAs: 'ctrl',
-      resolve: loadAdminBundle,
     })
     .when('/admin/orgs/edit/:id', {
       templateUrl: 'public/app/features/admin/partials/edit_org.html',
       controller: 'AdminEditOrgCtrl',
       controllerAs: 'ctrl',
-      resolve: loadAdminBundle,
     })
     .when('/admin/stats', {
-      templateUrl: 'public/app/features/admin/partials/stats.html',
-      controller: 'AdminStatsCtrl',
-      controllerAs: 'ctrl',
-      resolve: loadAdminBundle,
+      template: '<react-container />',
+      resolve: {
+        component: () => ServerStats,
+      },
     })
     // LOGIN / SIGNUP
     .when('/login', {
@@ -274,30 +225,31 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
       templateUrl: 'public/app/features/alerting/partials/alert_list.html',
       controller: 'AlertListCtrl',
       controllerAs: 'ctrl',
-      resolve: loadAlertingBundle,
+    })
+    .when('/alerting/list', {
+      template: '<react-container />',
+      reloadOnSearch: false,
+      resolve: {
+        component: () => AlertRuleList,
+      },
     })
     .when('/alerting/notifications', {
       templateUrl: 'public/app/features/alerting/partials/notifications_list.html',
       controller: 'AlertNotificationsListCtrl',
       controllerAs: 'ctrl',
-      resolve: loadAlertingBundle,
     })
     .when('/alerting/notification/new', {
       templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
       controller: 'AlertNotificationEditCtrl',
       controllerAs: 'ctrl',
-      resolve: loadAlertingBundle,
     })
     .when('/alerting/notification/:id/edit', {
       templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
       controller: 'AlertNotificationEditCtrl',
       controllerAs: 'ctrl',
-      resolve: loadAlertingBundle,
     })
     .otherwise({
       templateUrl: 'public/app/partials/error.html',
       controller: 'ErrorCtrl',
     });
 }
-
-coreModule.config(setupAngularRoutes);

+ 34 - 0
public/app/stores/AlertListStore/AlertListStore.ts

@@ -0,0 +1,34 @@
+import { types, getEnv, flow } from 'mobx-state-tree';
+import { AlertRule } from './AlertRule';
+import { setStateFields } from './helpers';
+
+type IAlertRuleType = typeof AlertRule.Type;
+export interface IAlertRule extends IAlertRuleType {}
+
+export const AlertListStore = types
+  .model('AlertListStore', {
+    rules: types.array(AlertRule),
+    stateFilter: types.optional(types.string, 'all'),
+  })
+  .actions(self => ({
+    loadRules: flow(function* load(filters) {
+      const backendSrv = getEnv(self).backendSrv;
+      self.stateFilter = filters.state; // store state filter used in api query
+      const apiRules = yield backendSrv.get('/api/alerts', filters);
+      self.rules.clear();
+
+      for (let rule of apiRules) {
+        setStateFields(rule, rule.state);
+
+        if (rule.executionError) {
+          rule.info = 'Execution Error: ' + rule.executionError;
+        }
+
+        if (rule.evalData && rule.evalData.noData) {
+          rule.info = 'Query returned no data';
+        }
+
+        self.rules.push(AlertRule.create(rule));
+      }
+    }),
+  }));

+ 34 - 0
public/app/stores/AlertListStore/AlertRule.ts

@@ -0,0 +1,34 @@
+import { types, getEnv, flow } from 'mobx-state-tree';
+import { setStateFields } from './helpers';
+
+export const AlertRule = types
+  .model('AlertRule', {
+    id: types.identifier(types.number),
+    dashboardId: types.number,
+    panelId: types.number,
+    name: types.string,
+    state: types.string,
+    stateText: types.string,
+    stateIcon: types.string,
+    stateClass: types.string,
+    stateAge: types.string,
+    info: types.optional(types.string, ''),
+    dashboardUri: types.string,
+  })
+  .views(self => ({
+    get isPaused() {
+      return self.state === 'paused';
+    },
+  }))
+  .actions(self => ({
+    /**
+     * will toggle alert rule paused state
+     */
+    togglePaused: flow(function* togglePaused() {
+      const backendSrv = getEnv(self).backendSrv;
+      const payload = { paused: self.isPaused };
+      const res = yield backendSrv.post(`/api/alerts/${self.id}/pause`, payload);
+      setStateFields(self, res.state);
+      self.info = '';
+    }),
+  }));

+ 13 - 0
public/app/stores/AlertListStore/helpers.ts

@@ -0,0 +1,13 @@
+import moment from 'moment';
+import alertDef from 'app/features/alerting/alert_def';
+
+export function setStateFields(rule, state) {
+  const stateModel = alertDef.getStateDisplayModel(state);
+  rule.state = state;
+  rule.stateText = stateModel.text;
+  rule.stateIcon = stateModel.iconClass;
+  rule.stateClass = stateModel.stateClass;
+  rule.stateAge = moment(rule.newStateDate)
+    .fromNow()
+    .replace(' ago', '');
+}

+ 12 - 0
public/app/stores/NavStore/NavItem.ts

@@ -0,0 +1,12 @@
+import { types } from 'mobx-state-tree';
+
+export const NavItem = types.model('NavItem', {
+  id: types.identifier(types.string),
+  text: types.string,
+  url: types.optional(types.string, ''),
+  subTitle: types.optional(types.string, ''),
+  icon: types.optional(types.string, ''),
+  img: types.optional(types.string, ''),
+  active: types.optional(types.boolean, false),
+  children: types.optional(types.array(types.late(() => NavItem)), []),
+});

+ 41 - 0
public/app/stores/NavStore/NavStore.ts

@@ -0,0 +1,41 @@
+import { types, getEnv } from 'mobx-state-tree';
+import { NavItem } from './NavItem';
+
+export const NavStore = types
+  .model('NavStore', {
+    main: types.maybe(NavItem),
+    node: types.maybe(NavItem),
+  })
+  .actions(self => ({
+    load(...args) {
+      let children = getEnv(self).navTree;
+      let main, node;
+      let parents = [];
+
+      for (let id of args) {
+        node = children.find(el => el.id === id);
+
+        if (!node) {
+          throw new Error(`NavItem with id ${id} not found`);
+        }
+
+        children = node.children;
+        parents.push(node);
+      }
+
+      main = parents[parents.length - 2];
+
+      if (main.children) {
+        for (let item of main.children) {
+          item.active = false;
+
+          if (item.url === node.url) {
+            item.active = true;
+          }
+        }
+      }
+
+      self.main = NavItem.create(main);
+      self.node = NavItem.create(node);
+    },
+  }));

+ 26 - 0
public/app/stores/RootStore/RootStore.ts

@@ -0,0 +1,26 @@
+import { types } from 'mobx-state-tree';
+import { SearchStore } from './../SearchStore/SearchStore';
+import { ServerStatsStore } from './../ServerStatsStore/ServerStatsStore';
+import { NavStore } from './../NavStore/NavStore';
+import { AlertListStore } from './../AlertListStore/AlertListStore';
+import { ViewStore } from './../ViewStore/ViewStore';
+
+export const RootStore = types.model({
+  search: types.optional(SearchStore, {
+    sections: [],
+  }),
+  serverStats: types.optional(ServerStatsStore, {
+    stats: [],
+  }),
+  nav: types.optional(NavStore, {}),
+  alertList: types.optional(AlertListStore, {
+    rules: [],
+  }),
+  view: types.optional(ViewStore, {
+    path: '',
+    query: {},
+  }),
+});
+
+type IRootStoreType = typeof RootStore.Type;
+export interface IRootStore extends IRootStoreType {}

+ 10 - 0
public/app/stores/SearchStore/ResultItem.ts

@@ -0,0 +1,10 @@
+import { types } from 'mobx-state-tree';
+
+export const ResultItem = types.model('ResultItem', {
+  id: types.identifier(types.number),
+  folderId: types.optional(types.number, 0),
+  title: types.string,
+  url: types.string,
+  icon: types.string,
+  folderTitle: types.optional(types.string, ''),
+});

+ 27 - 0
public/app/stores/SearchStore/SearchResultSection.ts

@@ -0,0 +1,27 @@
+import { types } from 'mobx-state-tree';
+import { ResultItem } from './ResultItem';
+
+export const SearchResultSection = types
+  .model('SearchResultSection', {
+    id: types.identifier(),
+    title: types.string,
+    icon: types.string,
+    expanded: types.boolean,
+    items: types.array(ResultItem),
+  })
+  .actions(self => ({
+    toggle() {
+      self.expanded = !self.expanded;
+
+      for (let i = 0; i < 100; i++) {
+        self.items.push(
+          ResultItem.create({
+            id: i,
+            title: 'Dashboard ' + self.items.length,
+            icon: 'gicon gicon-dashboard',
+            url: 'asd',
+          })
+        );
+      }
+    },
+  }));

+ 22 - 0
public/app/stores/SearchStore/SearchStore.ts

@@ -0,0 +1,22 @@
+import { types } from 'mobx-state-tree';
+import { SearchResultSection } from './SearchResultSection';
+
+export const SearchStore = types
+  .model('SearchStore', {
+    sections: types.array(SearchResultSection),
+  })
+  .actions(self => ({
+    query() {
+      for (let i = 0; i < 100; i++) {
+        self.sections.push(
+          SearchResultSection.create({
+            id: 'starred' + i,
+            title: 'starred',
+            icon: 'fa fa-fw fa-star-o',
+            expanded: false,
+            items: [],
+          })
+        );
+      }
+    },
+  }));

+ 6 - 0
public/app/stores/ServerStatsStore/ServerStat.ts

@@ -0,0 +1,6 @@
+import { types } from 'mobx-state-tree';
+
+export const ServerStat = types.model('ServerStat', {
+  name: types.string,
+  value: types.optional(types.number, 0),
+});

+ 24 - 0
public/app/stores/ServerStatsStore/ServerStatsStore.ts

@@ -0,0 +1,24 @@
+import { types, getEnv, flow } from 'mobx-state-tree';
+import { ServerStat } from './ServerStat';
+
+export const ServerStatsStore = types
+  .model('ServerStatsStore', {
+    stats: types.array(ServerStat),
+    error: types.optional(types.string, ''),
+  })
+  .actions(self => ({
+    load: flow(function* load() {
+      const backendSrv = getEnv(self).backendSrv;
+      const res = yield backendSrv.get('/api/admin/stats');
+      self.stats.clear();
+      self.stats.push(ServerStat.create({ name: 'Total dashboards', value: res.dashboards }));
+      self.stats.push(ServerStat.create({ name: 'Total users', value: res.users }));
+      self.stats.push(ServerStat.create({ name: 'Active users (seen last 30 days)', value: res.activeUsers }));
+      self.stats.push(ServerStat.create({ name: 'Total orgs', value: res.orgs }));
+      self.stats.push(ServerStat.create({ name: 'Total playlists', value: res.playlists }));
+      self.stats.push(ServerStat.create({ name: 'Total snapshots', value: res.snapshots }));
+      self.stats.push(ServerStat.create({ name: 'Total dashboard tags', value: res.tags }));
+      self.stats.push(ServerStat.create({ name: 'Total starred dashboards', value: res.stars }));
+      self.stats.push(ServerStat.create({ name: 'Total alerts', value: res.alerts }));
+    }),
+  }));

+ 46 - 0
public/app/stores/ViewStore/ViewStore.ts

@@ -0,0 +1,46 @@
+import { types } from 'mobx-state-tree';
+
+const QueryValueType = types.union(types.string, types.boolean, types.number);
+const urlParameterize = queryObj => {
+  const keys = Object.keys(queryObj);
+  const newQuery = keys.reduce((acc: string, key: string, idx: number) => {
+    const preChar = idx === 0 ? '?' : '&';
+    return acc + preChar + key + '=' + queryObj[key];
+  }, '');
+
+  return newQuery;
+};
+
+export const ViewStore = types
+  .model({
+    path: types.string,
+    query: types.map(QueryValueType),
+  })
+  .views(self => ({
+    get currentUrl() {
+      let path = self.path;
+
+      if (self.query.size) {
+        path += urlParameterize(self.query.toJS());
+      }
+      return path;
+    },
+  }))
+  .actions(self => {
+    function updateQuery(query: any) {
+      self.query.clear();
+      for (let key of Object.keys(query)) {
+        self.query.set(key, query[key]);
+      }
+    }
+
+    function updatePathAndQuery(path: string, query: any) {
+      self.path = path;
+      updateQuery(query);
+    }
+
+    return {
+      updateQuery,
+      updatePathAndQuery,
+    };
+  });

+ 16 - 0
public/app/stores/store.ts

@@ -0,0 +1,16 @@
+import { RootStore, IRootStore } from './RootStore/RootStore';
+import config from 'app/core/config';
+
+export let store: IRootStore;
+
+export function createStore(backendSrv) {
+  store = RootStore.create(
+    {},
+    {
+      backendSrv: backendSrv,
+      navTree: config.bootData.navTree,
+    }
+  );
+
+  return store;
+}

+ 5 - 5
public/sass/components/_gf-form.scss

@@ -217,7 +217,7 @@ $input-border: 1px solid $input-border-color;
       background-color: transparent;
       color: $text-color;
       font: normal normal normal $font-size-sm/1 FontAwesome;
-      content: "\f0d7";
+      content: '\f0d7';
       pointer-events: none;
     }
   }
@@ -270,7 +270,7 @@ $input-border: 1px solid $input-border-color;
 
   select.gf-form-input {
     text-indent: 0.01px;
-    text-overflow: "";
+    text-overflow: '';
     padding-right: $input-padding-x*3;
     appearance: none;
 
@@ -292,7 +292,7 @@ $input-border: 1px solid $input-border-color;
     background-color: transparent;
     color: $text-color;
     font: normal normal normal $font-size-sm/1 FontAwesome;
-    content: "\f0d7";
+    content: '\f0d7';
     pointer-events: none;
     font-size: 11px;
   }
@@ -319,7 +319,7 @@ $input-border: 1px solid $input-border-color;
 }
 
 .natural-language-input {
-  &input[type="number"] {
+  &input[type='number'] {
     font-size: $font-size-base;
     line-height: $input-line-height;
     margin: -6px -5px 0 5px;
@@ -342,7 +342,7 @@ $input-border: 1px solid $input-border-color;
     background-color: transparent;
     color: $input-color;
     font: normal normal normal $font-size-sm/1 FontAwesome;
-    content: "\f0d7";
+    content: '\f0d7';
     pointer-events: none;
   }
 

+ 2 - 3
public/sass/components/_page_header.scss

@@ -140,7 +140,7 @@
   }
 
   &::after {
-    content: "";
+    content: '';
     position: absolute;
     top: 0;
     right: -14px; // half of square's length
@@ -157,8 +157,7 @@
     background: linear-gradient(135deg, $btn-inverse-bg, $btn-inverse-bg-hl);
 
     // stylish arrow design using box shadow
-    box-shadow: 2px -2px 0 2px rgb(35, 31, 31),
-      3px -3px 0 2px rgba(255, 255, 255, 0.1);
+    box-shadow: 2px -2px 0 2px rgb(35, 31, 31), 3px -3px 0 2px rgba(255, 255, 255, 0.1);
 
     // 5px - for rounded arrows and
     // 50px - to prevent hover glitches on the border created using shadows*/

+ 5 - 7
public/sass/pages/_alerting.scss

@@ -29,7 +29,6 @@
 }
 
 // Alert List
-
 .alert-list {
   display: flex;
   flex-direction: row;
@@ -94,7 +93,7 @@
 
 .panel-has-alert {
   .panel-alert-icon:before {
-    content: "\e611";
+    content: '\e611';
     position: relative;
     top: 1px;
     left: -3px;
@@ -108,12 +107,12 @@
 
     .panel-alert-icon:before {
       color: $critical;
-      content: "\e610";
+      content: '\e610';
     }
   }
 
   &--alerting::after {
-    content: "";
+    content: '';
     position: absolute;
     top: 0;
     z-index: -1;
@@ -121,14 +120,13 @@
     height: 100%;
     box-shadow: 0 0 10px rgba($critical, 1);
     opacity: 0;
-    animation: alerting-panel 1.6s cubic-bezier(1, 0.1, 0.73, 1) 0s infinite
-      alternate;
+    animation: alerting-panel 1.6s cubic-bezier(1, 0.1, 0.73, 1) 0s infinite alternate;
   }
 
   &--ok {
     .panel-alert-icon:before {
       color: $online;
-      content: "\e611";
+      content: '\e611';
     }
   }
 }

+ 15 - 0
public/test/mocks/common.ts

@@ -0,0 +1,15 @@
+export const backendSrv = {
+  get: jest.fn(),
+  post: jest.fn(),
+};
+
+export function createNavTree(...args) {
+  let root = [];
+  let node = root;
+  for (let arg of args) {
+    let child = { id: arg, url: `/url/${arg}`, text: `${arg}-Text`, children: [] };
+    node.push(child);
+    node = child.children;
+  }
+  return root;
+}

+ 1 - 1
tsconfig.json

@@ -13,7 +13,7 @@
       "sourceMap": true,
       "noEmitOnError": false,
       "emitDecoratorMetadata": false,
-      "experimentalDecorators": false,
+      "experimentalDecorators": true,
       "noImplicitReturns": true,
       "noImplicitThis": false,
       "noImplicitUseStrict":false,

Разница между файлами не показана из-за своего большого размера
+ 351 - 213
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов