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
This commit is contained in:
Johannes Schill 2018-01-03 20:11:07 +01:00 committed by Torkel Ödegaard
parent f049fc4816
commit 8abef88b94
22 changed files with 256 additions and 231 deletions

View File

@ -1,7 +1,7 @@
import React from 'react';
import moment from 'moment';
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 { mount } from 'enzyme';
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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = '';
}),
}));

View File

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

View File

@ -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)), []),
});

View File

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

View File

@ -1,9 +1,9 @@
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({
search: types.optional(SearchStore, {

View File

@ -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: []
})
);
}
}
}));

View File

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

View File

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

View File

@ -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: [],
})
);
}
},
}));

View File

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

View File

@ -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
.model('ServerStatsStore', {
@ -12,9 +8,8 @@ export const ServerStatsStore = types
})
.actions(self => ({
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.push(ServerStat.create({ name: 'Total dashboards', value: res.dashboards }));
self.stats.push(ServerStat.create({ name: 'Total users', value: res.users }));

View File

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

View File

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

View File

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