Просмотр исходного кода

mobx: poc in using each store as individual prop on the react containers (#10414)

* mobx: poc in using each store as individual prop on the react containers

* prettier test

* fix: end the war between prettier vs tslint.

* mobx: Move stores into their own folders

* mobx: Refactor the AlertRule into its own file and add a helper-file

* mobx: Move NavItem out of NavStore and remove lodash dependancy

* mobx: Move ResultItem and SearchResultSection models out of the SearchStore

* mobx: ServerStatsStore rename .tsx => .ts. And move ServerStat-model to its own file.

* mobx: Remove lodash and jquery dependancy from ViewStore

* mobx: Remove issue with double question mark
Johannes Schill 8 лет назад
Родитель
Сommit
8abef88b94

+ 2 - 2
public/app/containers/AlertRuleList/AlertRuleList.jest.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import moment from 'moment';
 import moment from 'moment';
 import { AlertRuleList } from './AlertRuleList';
 import { AlertRuleList } from './AlertRuleList';
-import { RootStore } from 'app/stores/RootStore';
+import { RootStore } from 'app/stores/RootStore/RootStore';
 import { backendSrv, createNavTree } from 'test/mocks/common';
 import { backendSrv, createNavTree } from 'test/mocks/common';
 import { mount } from 'enzyme';
 import { mount } from 'enzyme';
 import toJson from 'enzyme-to-json';
 import toJson from 'enzyme-to-json';
@@ -36,7 +36,7 @@ describe('AlertRuleList', () => {
       }
       }
     );
     );
 
 
-    page = mount(<AlertRuleList store={store} />);
+    page = mount(<AlertRuleList {...store} />);
   });
   });
 
 
   it('should call api to get rules', () => {
   it('should call api to get rules', () => {

+ 9 - 13
public/app/containers/AlertRuleList/AlertRuleList.tsx

@@ -2,17 +2,13 @@ import React from 'react';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import { inject, observer } from 'mobx-react';
 import { inject, observer } from 'mobx-react';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import { IRootStore } from 'app/stores/RootStore';
-import { IAlertRule } from 'app/stores/AlertListStore';
+import { IAlertRule } from 'app/stores/AlertListStore/AlertListStore';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
+import IContainerProps from 'app/containers/IContainerProps';
 
 
-export interface AlertRuleListProps {
-  store: IRootStore;
-}
-
-@inject('store')
+@inject('view', 'nav', 'alertList')
 @observer
 @observer
-export class AlertRuleList extends React.Component<AlertRuleListProps, any> {
+export class AlertRuleList extends React.Component<IContainerProps, any> {
   stateFilters = [
   stateFilters = [
     { text: 'All', value: 'all' },
     { text: 'All', value: 'all' },
     { text: 'OK', value: 'ok' },
     { text: 'OK', value: 'ok' },
@@ -25,18 +21,18 @@ export class AlertRuleList extends React.Component<AlertRuleListProps, any> {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.props.store.nav.load('alerting', 'alert-list');
+    this.props.nav.load('alerting', 'alert-list');
     this.fetchRules();
     this.fetchRules();
   }
   }
 
 
   onStateFilterChanged = evt => {
   onStateFilterChanged = evt => {
-    this.props.store.view.updateQuery({ state: evt.target.value });
+    this.props.view.updateQuery({ state: evt.target.value });
     this.fetchRules();
     this.fetchRules();
   };
   };
 
 
   fetchRules() {
   fetchRules() {
-    this.props.store.alertList.loadRules({
-      state: this.props.store.view.query.get('state') || 'all',
+    this.props.alertList.loadRules({
+      state: this.props.view.query.get('state') || 'all',
     });
     });
   }
   }
 
 
@@ -49,7 +45,7 @@ export class AlertRuleList extends React.Component<AlertRuleListProps, any> {
   };
   };
 
 
   render() {
   render() {
-    const { nav, alertList } = this.props.store;
+    const { nav, alertList } = this.props;
 
 
     return (
     return (
       <div>
       <div>

+ 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;

+ 2 - 2
public/app/containers/ServerStats/ServerStats.jest.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import renderer from 'react-test-renderer';
 import renderer from 'react-test-renderer';
 import { ServerStats } from './ServerStats';
 import { ServerStats } from './ServerStats';
-import { RootStore } from 'app/stores/RootStore';
+import { RootStore } from 'app/stores/RootStore/RootStore';
 import { backendSrv, createNavTree } from 'test/mocks/common';
 import { backendSrv, createNavTree } from 'test/mocks/common';
 
 
 describe('ServerStats', () => {
 describe('ServerStats', () => {
@@ -20,7 +20,7 @@ describe('ServerStats', () => {
       }
       }
     );
     );
 
 
-    const page = renderer.create(<ServerStats store={store} />);
+    const page = renderer.create(<ServerStats {...store} />);
 
 
     setTimeout(() => {
     setTimeout(() => {
       expect(page.toJSON()).toMatchSnapshot();
       expect(page.toJSON()).toMatchSnapshot();

+ 9 - 10
public/app/containers/ServerStats/ServerStats.tsx

@@ -1,25 +1,24 @@
 import React from 'react';
 import React from 'react';
 import { inject, observer } from 'mobx-react';
 import { inject, observer } from 'mobx-react';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import IContainerProps from 'app/containers/IContainerProps';
 
 
-export interface IProps {
-  store: any;
-}
-
-@inject('store')
+@inject('nav', 'serverStats')
 @observer
 @observer
-export class ServerStats extends React.Component<IProps, any> {
+export class ServerStats extends React.Component<IContainerProps, any> {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
+    const { nav, serverStats } = this.props;
 
 
-    this.props.store.nav.load('cfg', 'admin', 'server-stats');
-    this.props.store.serverStats.load();
+    nav.load('cfg', 'admin', 'server-stats');
+    serverStats.load();
   }
   }
 
 
   render() {
   render() {
+    const { nav, serverStats } = this.props;
     return (
     return (
       <div>
       <div>
-        <PageHeader model={this.props.store.nav} />
+        <PageHeader model={nav as any} />
         <div className="page-container page-body">
         <div className="page-container page-body">
           <table className="filter-table form-inline">
           <table className="filter-table form-inline">
             <thead>
             <thead>
@@ -28,7 +27,7 @@ export class ServerStats extends React.Component<IProps, any> {
                 <th>Value</th>
                 <th>Value</th>
               </tr>
               </tr>
             </thead>
             </thead>
-            <tbody>{this.props.store.serverStats.stats.map(StatItem)}</tbody>
+            <tbody>{serverStats.stats.map(StatItem)}</tbody>
           </table>
           </table>
         </div>
         </div>
       </div>
       </div>

+ 1 - 1
public/app/routes/ReactContainer.tsx

@@ -6,7 +6,7 @@ import { Provider } from 'mobx-react';
 
 
 function WrapInProvider(store, Component, props) {
 function WrapInProvider(store, Component, props) {
   return (
   return (
-    <Provider store={store}>
+    <Provider {...store}>
       <Component {...props} />
       <Component {...props} />
     </Provider>
     </Provider>
   );
   );

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

@@ -1,82 +0,0 @@
-import { types, getEnv, flow } from 'mobx-state-tree';
-import moment from 'moment';
-import alertDef from 'app/features/alerting/alert_def';
-
-function setStateFields(rule, state) {
-  let 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', '');
-}
-
-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() {
-      let backendSrv = getEnv(self).backendSrv;
-
-      var payload = { paused: self.isPaused };
-      let res = yield backendSrv.post(`/api/alerts/${self.id}/pause`, payload);
-      setStateFields(self, res.state);
-      self.info = '';
-    }),
-  }));
-
-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) {
-      let backendSrv = getEnv(self).backendSrv;
-
-      // store state filter used in api query
-      self.stateFilter = filters.state;
-
-      let 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/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)), []),
+});

+ 4 - 14
public/app/stores/NavStore.ts → public/app/stores/NavStore/NavStore.ts

@@ -1,16 +1,5 @@
 import { types, getEnv } from 'mobx-state-tree';
 import { types, getEnv } from 'mobx-state-tree';
-import _ from 'lodash';
-
-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)), []),
-});
+import { NavItem } from './NavItem';
 
 
 export const NavStore = types
 export const NavStore = types
   .model('NavStore', {
   .model('NavStore', {
@@ -19,12 +8,13 @@ export const NavStore = types
   })
   })
   .actions(self => ({
   .actions(self => ({
     load(...args) {
     load(...args) {
-      var children = getEnv(self).navTree;
+      let children = getEnv(self).navTree;
       let main, node;
       let main, node;
       let parents = [];
       let parents = [];
 
 
       for (let id of args) {
       for (let id of args) {
-        node = _.find(children, { id: id });
+        node = children.find(el => el.id === id);
+
         if (!node) {
         if (!node) {
           throw new Error(`NavItem with id ${id} not found`);
           throw new Error(`NavItem with id ${id} not found`);
         }
         }

+ 5 - 5
public/app/stores/RootStore.ts → public/app/stores/RootStore/RootStore.ts

@@ -1,9 +1,9 @@
 import { types } from 'mobx-state-tree';
 import { types } from 'mobx-state-tree';
-import { SearchStore } from './SearchStore';
-import { ServerStatsStore } from './ServerStatsStore';
-import { NavStore } from './NavStore';
-import { AlertListStore } from './AlertListStore';
-import { ViewStore } from './ViewStore';
+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({
 export const RootStore = types.model({
   search: types.optional(SearchStore, {
   search: types.optional(SearchStore, {

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

@@ -1,55 +0,0 @@
-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, "")
-});
-
-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"
-          })
-        );
-      }
-    }
-  }));
-
-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: []
-          })
-        );
-      }
-    }
-  }));

+ 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),
+});

+ 4 - 9
public/app/stores/ServerStatsStore.tsx → public/app/stores/ServerStatsStore/ServerStatsStore.ts

@@ -1,9 +1,5 @@
-import { types, getEnv, flow } from 'mobx-state-tree';
-
-export const ServerStat = types.model('ServerStat', {
-  name: types.string,
-  value: types.optional(types.number, 0),
-});
+import { types, getEnv, flow } from 'mobx-state-tree';
+import { ServerStat } from './ServerStat';
 
 
 export const ServerStatsStore = types
 export const ServerStatsStore = types
   .model('ServerStatsStore', {
   .model('ServerStatsStore', {
@@ -12,9 +8,8 @@ export const ServerStatsStore = types
   })
   })
   .actions(self => ({
   .actions(self => ({
     load: flow(function* load() {
     load: flow(function* load() {
-      let backendSrv = getEnv(self).backendSrv;
-
-      let res = yield backendSrv.get('/api/admin/stats');
+      const backendSrv = getEnv(self).backendSrv;
+      const res = yield backendSrv.get('/api/admin/stats');
       self.stats.clear();
       self.stats.clear();
       self.stats.push(ServerStat.create({ name: 'Total dashboards', value: res.dashboards }));
       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: 'Total users', value: res.users }));

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

@@ -1,37 +0,0 @@
-import { types } from 'mobx-state-tree';
-import _ from 'lodash';
-import $ from 'jquery';
-
-let QueryValueType = types.union(types.string, types.boolean, types.number);
-
-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 += '?' + $.param(self.query.toJS());
-      }
-      return path;
-    },
-  }))
-  .actions(self => ({
-    updatePathAndQuery(path: string, query: any) {
-      self.path = path;
-      self.query.clear();
-
-      for (let key of _.keys(query)) {
-        self.query.set(key, query[key]);
-      }
-    },
-
-    updateQuery(query: any) {
-      self.query.clear();
-      for (let key of _.keys(query)) {
-        self.query.set(key, query[key]);
-      }
-    },
-  }));

+ 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,
+    };
+  });

+ 1 - 1
public/app/stores/store.ts

@@ -1,4 +1,4 @@
-import { RootStore, IRootStore } from './RootStore';
+import { RootStore, IRootStore } from './RootStore/RootStore';
 import config from 'app/core/config';
 import config from 'app/core/config';
 
 
 export let store: IRootStore;
 export let store: IRootStore;