mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 00:25:46 -06:00
Merge branch 'redux-poc2'
This commit is contained in:
commit
298c088d57
@ -72,6 +72,7 @@ email = "email"
|
||||
[[servers.group_mappings]]
|
||||
group_dn = "cn=admins,ou=groups,dc=grafana,dc=org"
|
||||
org_role = "Admin"
|
||||
grafana_admin = true
|
||||
# The Grafana organization database id, optional, if left out the default org (id 1) will be used
|
||||
# org_id = 1
|
||||
|
||||
|
@ -160,9 +160,13 @@
|
||||
"react-grid-layout": "0.16.6",
|
||||
"react-highlight-words": "^0.10.0",
|
||||
"react-popper": "^0.7.5",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-select": "^1.1.0",
|
||||
"react-sizeme": "^2.3.6",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"redux": "^4.0.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"remarkable": "^1.7.1",
|
||||
"rst2html": "github:thoward/rst2html#990cb89",
|
||||
"rxjs": "^5.4.3",
|
||||
|
@ -1,69 +0,0 @@
|
||||
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: '',
|
||||
url: 'd/ufkcofof/my-goal',
|
||||
canEdit: true,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
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();
|
||||
const ruleNode = page.find('.alert-rule-item');
|
||||
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);
|
||||
});
|
||||
});
|
@ -1,178 +0,0 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import classNames from 'classnames';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import { AlertRule } from 'app/stores/AlertListStore/AlertListStore';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import ContainerProps from 'app/containers/ContainerProps';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
|
||||
@inject('view', 'nav', 'alertList')
|
||||
@observer
|
||||
export class AlertRuleList extends React.Component<ContainerProps, 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: {},
|
||||
});
|
||||
};
|
||||
|
||||
onSearchQueryChange = evt => {
|
||||
this.props.alertList.setSearchQuery(evt.target.value);
|
||||
};
|
||||
|
||||
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 gf-form--grow">
|
||||
<label className="gf-form--has-input-icon gf-form--grow">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search alerts"
|
||||
value={alertList.search}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label">States</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>
|
||||
<ol className="alert-rule-list">
|
||||
{alertList.filteredRules.map(rule => (
|
||||
<AlertRuleItem rule={rule} key={rule.id} search={alertList.search} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function AlertStateFilterOption({ text, value }) {
|
||||
return (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
|
||||
export interface AlertRuleItemProps {
|
||||
rule: AlertRule;
|
||||
search: string;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> {
|
||||
toggleState = () => {
|
||||
this.props.rule.togglePaused();
|
||||
};
|
||||
|
||||
renderText(text: string) {
|
||||
return (
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
textToHighlight={text}
|
||||
searchWords={[this.props.search]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { rule } = this.props;
|
||||
|
||||
const stateClass = classNames({
|
||||
fa: true,
|
||||
'fa-play': rule.isPaused,
|
||||
'fa-pause': !rule.isPaused,
|
||||
});
|
||||
|
||||
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
|
||||
|
||||
return (
|
||||
<li className="alert-rule-item">
|
||||
<span className={`alert-rule-item__icon ${rule.stateClass}`}>
|
||||
<i className={rule.stateIcon} />
|
||||
</span>
|
||||
<div className="alert-rule-item__body">
|
||||
<div className="alert-rule-item__header">
|
||||
<div className="alert-rule-item__name">
|
||||
<a href={ruleUrl}>{this.renderText(rule.name)}</a>
|
||||
</div>
|
||||
<div className="alert-rule-item__text">
|
||||
<span className={`${rule.stateClass}`}>{this.renderText(rule.stateText)}</span>
|
||||
<span className="alert-rule-item__time"> for {rule.stateAge}</span>
|
||||
</div>
|
||||
</div>
|
||||
{rule.info && <div className="small muted alert-rule-item__info">{this.renderText(rule.info)}</div>}
|
||||
</div>
|
||||
|
||||
<div className="alert-rule-item__actions">
|
||||
<button
|
||||
className="btn btn-small btn-inverse alert-list__btn width-2"
|
||||
title="Pausing an alert rule prevents it from executing"
|
||||
onClick={this.toggleState}
|
||||
>
|
||||
<i className={stateClass} />
|
||||
</button>
|
||||
<a className="btn btn-small btn-inverse alert-list__btn width-2" href={ruleUrl} title="Edit alert rule">
|
||||
<i className="icon-gf icon-gf-settings" />
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(AlertRuleList);
|
@ -1,16 +1,10 @@
|
||||
import { SearchStore } from './../stores/SearchStore/SearchStore';
|
||||
import { ServerStatsStore } from './../stores/ServerStatsStore/ServerStatsStore';
|
||||
import { NavStore } from './../stores/NavStore/NavStore';
|
||||
import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore';
|
||||
import { AlertListStore } from './../stores/AlertListStore/AlertListStore';
|
||||
import { ViewStore } from './../stores/ViewStore/ViewStore';
|
||||
import { FolderStore } from './../stores/FolderStore/FolderStore';
|
||||
|
||||
interface ContainerProps {
|
||||
search: typeof SearchStore.Type;
|
||||
serverStats: typeof ServerStatsStore.Type;
|
||||
nav: typeof NavStore.Type;
|
||||
alertList: typeof AlertListStore.Type;
|
||||
permissions: typeof PermissionsStore.Type;
|
||||
view: typeof ViewStore.Type;
|
||||
folder: typeof FolderStore.Type;
|
||||
|
@ -1,30 +0,0 @@
|
||||
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 backendSrv={backendSrv} {...store} />);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(page.toJSON()).toMatchSnapshot();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import ContainerProps from 'app/containers/ContainerProps';
|
||||
|
||||
@inject('nav', 'serverStats')
|
||||
@observer
|
||||
export class ServerStats extends React.Component<ContainerProps, 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>
|
||||
);
|
||||
}
|
||||
|
||||
export default hot(module)(ServerStats);
|
3
public/app/core/actions/index.ts
Normal file
3
public/app/core/actions/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { updateLocation } from './location';
|
||||
|
||||
export { updateLocation };
|
13
public/app/core/actions/location.ts
Normal file
13
public/app/core/actions/location.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { LocationUpdate } from 'app/types';
|
||||
|
||||
export type Action = UpdateLocationAction;
|
||||
|
||||
export interface UpdateLocationAction {
|
||||
type: 'UPDATE_LOCATION';
|
||||
payload: LocationUpdate;
|
||||
}
|
||||
|
||||
export const updateLocation = (location: LocationUpdate): UpdateLocationAction => ({
|
||||
type: 'UPDATE_LOCATION',
|
||||
payload: location,
|
||||
});
|
13
public/app/core/actions/navModel.ts
Normal file
13
public/app/core/actions/navModel.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export type Action = UpdateNavIndexAction;
|
||||
|
||||
// this action is not used yet
|
||||
// kind of just a placeholder, will be need for dynamic pages
|
||||
// like datasource edit, teams edit page
|
||||
|
||||
export interface UpdateNavIndexAction {
|
||||
type: 'UPDATE_NAV_INDEX';
|
||||
}
|
||||
|
||||
export const updateNavIndex = (): UpdateNavIndexAction => ({
|
||||
type: 'UPDATE_NAV_INDEX',
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { NavModel, NavModelItem } from '../../nav_model_srv';
|
||||
import { NavModel, NavModelItem } from 'app/types';
|
||||
import classNames from 'classnames';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { toJS } from 'mobx';
|
||||
|
@ -10,6 +10,7 @@ import { createStore } from 'app/stores/store';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { configureStore } from 'app/stores/configureStore';
|
||||
|
||||
export class GrafanaCtrl {
|
||||
/** @ngInject */
|
||||
@ -25,6 +26,7 @@ export class GrafanaCtrl {
|
||||
datasourceSrv: DatasourceSrv
|
||||
) {
|
||||
// sets singleston instances for angular services so react components can access them
|
||||
configureStore();
|
||||
setBackendSrv(backendSrv);
|
||||
createStore({ backendSrv, datasourceSrv });
|
||||
|
||||
|
@ -1,22 +1,13 @@
|
||||
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> {
|
||||
export class SearchResult extends React.Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
search: store.search,
|
||||
search: '',
|
||||
};
|
||||
|
||||
store.search.query();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -30,7 +21,6 @@ export interface SectionProps {
|
||||
section: any;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class SearchResultSection extends React.Component<SectionProps, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
7
public/app/core/reducers/index.ts
Normal file
7
public/app/core/reducers/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { navIndexReducer as navIndex } from './navModel';
|
||||
import { locationReducer as location } from './location';
|
||||
|
||||
export default {
|
||||
navIndex,
|
||||
location,
|
||||
};
|
33
public/app/core/reducers/location.ts
Normal file
33
public/app/core/reducers/location.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Action } from 'app/core/actions/location';
|
||||
import { LocationState, UrlQueryMap } from 'app/types';
|
||||
import { toUrlParams } from 'app/core/utils/url';
|
||||
|
||||
export const initialState: LocationState = {
|
||||
url: '',
|
||||
path: '',
|
||||
query: {},
|
||||
routeParams: {},
|
||||
};
|
||||
|
||||
function renderUrl(path: string, query: UrlQueryMap): string {
|
||||
if (Object.keys(query).length > 0) {
|
||||
path += '?' + toUrlParams(query);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export const locationReducer = (state = initialState, action: Action): LocationState => {
|
||||
switch (action.type) {
|
||||
case 'UPDATE_LOCATION': {
|
||||
const { path, query, routeParams } = action.payload;
|
||||
return {
|
||||
url: renderUrl(path || state.path, query),
|
||||
path: path || state.path,
|
||||
query: query || state.query,
|
||||
routeParams: routeParams || state.routeParams,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
29
public/app/core/reducers/navModel.ts
Normal file
29
public/app/core/reducers/navModel.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Action } from 'app/core/actions/navModel';
|
||||
import { NavModelItem, NavIndex } from 'app/types';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export function buildInitialState(): NavIndex {
|
||||
const navIndex: NavIndex = {};
|
||||
const rootNodes = config.bootData.navTree as NavModelItem[];
|
||||
buildNavIndex(navIndex, rootNodes);
|
||||
return navIndex;
|
||||
}
|
||||
|
||||
function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?: NavModelItem) {
|
||||
for (const node of children) {
|
||||
navIndex[node.id] = {
|
||||
...node,
|
||||
parentItem: parentItem,
|
||||
};
|
||||
|
||||
if (node.children) {
|
||||
buildNavIndex(navIndex, node.children, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const initialState: NavIndex = buildInitialState();
|
||||
|
||||
export const navIndexReducer = (state = initialState, action: Action): NavIndex => {
|
||||
return state;
|
||||
};
|
39
public/app/core/selectors/navModel.ts
Normal file
39
public/app/core/selectors/navModel.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { NavModel, NavModelItem, NavIndex } from 'app/types';
|
||||
|
||||
function getNotFoundModel(): NavModel {
|
||||
const node: NavModelItem = {
|
||||
id: 'not-found',
|
||||
text: 'Page not found',
|
||||
icon: 'fa fa-fw fa-warning',
|
||||
subTitle: '404 Error',
|
||||
url: 'not-found',
|
||||
};
|
||||
|
||||
return {
|
||||
node: node,
|
||||
main: node,
|
||||
};
|
||||
}
|
||||
|
||||
export function getNavModel(navIndex: NavIndex, id: string): NavModel {
|
||||
if (navIndex[id]) {
|
||||
const node = navIndex[id];
|
||||
const main = {
|
||||
...node.parentItem,
|
||||
};
|
||||
|
||||
main.children = main.children.map(item => {
|
||||
return {
|
||||
...item,
|
||||
active: item.url === node.url,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
node: node,
|
||||
main: main,
|
||||
};
|
||||
} else {
|
||||
return getNotFoundModel();
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { store } from 'app/stores/store';
|
||||
import { store as reduxStore } from 'app/stores/configureStore';
|
||||
import { reaction } from 'mobx';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Services that handles angular -> mobx store sync & other react <-> angular sync
|
||||
export class BridgeSrv {
|
||||
@ -19,12 +21,30 @@ export class BridgeSrv {
|
||||
if (store.view.currentUrl !== angularUrl) {
|
||||
store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params);
|
||||
}
|
||||
const state = reduxStore.getState();
|
||||
if (state.location.url !== angularUrl) {
|
||||
reduxStore.dispatch(
|
||||
updateLocation({
|
||||
path: this.$location.path(),
|
||||
query: this.$location.search(),
|
||||
routeParams: this.$route.current.params,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
|
||||
store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params);
|
||||
reduxStore.dispatch(
|
||||
updateLocation({
|
||||
path: this.$location.path(),
|
||||
query: this.$location.search(),
|
||||
routeParams: this.$route.current.params,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// listen for mobx store changes and update angular
|
||||
reaction(
|
||||
() => store.view.currentUrl,
|
||||
currentUrl => {
|
||||
@ -39,6 +59,19 @@ export class BridgeSrv {
|
||||
}
|
||||
);
|
||||
|
||||
// Listen for changes in redux location -> update angular location
|
||||
reduxStore.subscribe(() => {
|
||||
const state = reduxStore.getState();
|
||||
const angularUrl = this.$location.url();
|
||||
const url = locationUtil.stripBaseFromUrl(state.location.url);
|
||||
if (angularUrl !== url) {
|
||||
this.$timeout(() => {
|
||||
this.$location.url(url);
|
||||
});
|
||||
console.log('store updating angular $location.url', url);
|
||||
}
|
||||
});
|
||||
|
||||
appEvents.on('location-change', payload => {
|
||||
const urlWithoutBase = locationUtil.stripBaseFromUrl(payload.href);
|
||||
if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import angular from 'angular';
|
||||
|
||||
export class AdminEditOrgCtrl {
|
||||
export default class AdminEditOrgCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
|
||||
$scope.init = () => {
|
||||
@ -48,4 +47,3 @@ export class AdminEditOrgCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('grafana.controllers').controller('AdminEditOrgCtrl', AdminEditOrgCtrl);
|
@ -1,7 +1,6 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class AdminEditUserCtrl {
|
||||
export default class AdminEditUserCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
|
||||
$scope.user = {};
|
||||
@ -117,5 +116,3 @@ export class AdminEditUserCtrl {
|
||||
$scope.init();
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('grafana.controllers').controller('AdminEditUserCtrl', AdminEditUserCtrl);
|
@ -1,6 +1,5 @@
|
||||
import angular from 'angular';
|
||||
|
||||
export class AdminListOrgsCtrl {
|
||||
export default class AdminListOrgsCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, backendSrv, navModelSrv) {
|
||||
$scope.init = () => {
|
||||
@ -33,4 +32,3 @@ export class AdminListOrgsCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('grafana.controllers').controller('AdminListOrgsCtrl', AdminListOrgsCtrl);
|
23
public/app/features/admin/ServerStats.test.tsx
Normal file
23
public/app/features/admin/ServerStats.test.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { ServerStats } from './ServerStats';
|
||||
import { createNavModel } from 'test/mocks/common';
|
||||
import { ServerStat } from './state/apis';
|
||||
|
||||
describe('ServerStats', () => {
|
||||
it('Should render table with stats', done => {
|
||||
const navModel = createNavModel('Admin', 'stats');
|
||||
const stats: ServerStat[] = [{ name: 'Total dashboards', value: 10 }, { name: 'Total Users', value: 1 }];
|
||||
|
||||
const getServerStats = () => {
|
||||
return Promise.resolve(stats);
|
||||
};
|
||||
|
||||
const page = renderer.create(<ServerStats navModel={navModel} getServerStats={getServerStats} />);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(page.toJSON()).toMatchSnapshot();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
73
public/app/features/admin/ServerStats.tsx
Normal file
73
public/app/features/admin/ServerStats.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { NavModel, StoreState } from 'app/types';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { getServerStats, ServerStat } from './state/apis';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
|
||||
interface Props {
|
||||
navModel: NavModel;
|
||||
getServerStats: () => Promise<ServerStat[]>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
stats: ServerStat[];
|
||||
}
|
||||
|
||||
export class ServerStats extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
stats: [],
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
try {
|
||||
const stats = await this.props.getServerStats();
|
||||
this.setState({ stats });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navModel } = this.props;
|
||||
const { stats } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={navModel} />
|
||||
<div className="page-container page-body">
|
||||
<table className="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{stats.map(StatItem)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function StatItem(stat: ServerStat) {
|
||||
return (
|
||||
<tr key={stat.name}>
|
||||
<td>{stat.name}</td>
|
||||
<td>{stat.value}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
navModel: getNavModel(state.navIndex, 'server-stats'),
|
||||
getServerStats: getServerStats,
|
||||
});
|
||||
|
||||
export default hot(module)(connect(mapStateToProps)(ServerStats));
|
@ -17,8 +17,9 @@ exports[`ServerStats Should render table with stats 1`] = `
|
||||
<span
|
||||
className="page-header__logo"
|
||||
>
|
||||
|
||||
|
||||
<i
|
||||
className="page-header__icon fa fa-fw fa-warning"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
className="page-header__info-block"
|
||||
@ -26,9 +27,13 @@ exports[`ServerStats Should render table with stats 1`] = `
|
||||
<h1
|
||||
className="page-header__title"
|
||||
>
|
||||
admin-Text
|
||||
Admin
|
||||
</h1>
|
||||
|
||||
<div
|
||||
className="page-header__sub-title"
|
||||
>
|
||||
subTitle
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
@ -36,19 +41,19 @@ exports[`ServerStats Should render table with stats 1`] = `
|
||||
className="gf-form-select-wrapper width-20 page-header__select-nav"
|
||||
>
|
||||
<label
|
||||
className="gf-form-select-icon "
|
||||
className="gf-form-select-icon icon"
|
||||
htmlFor="page-header-select-nav"
|
||||
/>
|
||||
<select
|
||||
className="gf-select-nav gf-form-input"
|
||||
id="page-header-select-nav"
|
||||
onChange={[Function]}
|
||||
value="/url/server-stats"
|
||||
value="Admin"
|
||||
>
|
||||
<option
|
||||
value="/url/server-stats"
|
||||
value="Admin"
|
||||
>
|
||||
server-stats-Text
|
||||
Admin
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -60,13 +65,13 @@ exports[`ServerStats Should render table with stats 1`] = `
|
||||
>
|
||||
<a
|
||||
className="gf-tabs-link active"
|
||||
href="/url/server-stats"
|
||||
href="Admin"
|
||||
target={undefined}
|
||||
>
|
||||
<i
|
||||
className=""
|
||||
className="icon"
|
||||
/>
|
||||
server-stats-Text
|
||||
Admin
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@ -101,66 +106,10 @@ exports[`ServerStats Should render table with stats 1`] = `
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Total users
|
||||
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
|
||||
1
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
@ -1,7 +1,7 @@
|
||||
import AdminListUsersCtrl from './admin_list_users_ctrl';
|
||||
import './admin_list_orgs_ctrl';
|
||||
import './admin_edit_org_ctrl';
|
||||
import './admin_edit_user_ctrl';
|
||||
import AdminListUsersCtrl from './AdminListUsersCtrl';
|
||||
import AdminEditUserCtrl from './AdminEditUserCtrl';
|
||||
import AdminListOrgsCtrl from './AdminListOrgsCtrl';
|
||||
import AdminEditOrgCtrl from './AdminEditOrgCtrl';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
@ -27,21 +27,11 @@ class AdminHomeCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
export class AdminStatsCtrl {
|
||||
stats: any;
|
||||
navModel: any;
|
||||
coreModule.controller('AdminListUsersCtrl', AdminListUsersCtrl);
|
||||
coreModule.controller('AdminEditUserCtrl', AdminEditUserCtrl);
|
||||
|
||||
/** @ngInject */
|
||||
constructor(backendSrv: any, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'admin', 'server-stats', 1);
|
||||
|
||||
backendSrv.get('/api/admin/stats').then(stats => {
|
||||
this.stats = stats;
|
||||
});
|
||||
}
|
||||
}
|
||||
coreModule.controller('AdminListOrgsCtrl', AdminListOrgsCtrl);
|
||||
coreModule.controller('AdminEditOrgCtrl', AdminEditOrgCtrl);
|
||||
|
||||
coreModule.controller('AdminSettingsCtrl', AdminSettingsCtrl);
|
||||
coreModule.controller('AdminHomeCtrl', AdminHomeCtrl);
|
||||
coreModule.controller('AdminStatsCtrl', AdminStatsCtrl);
|
||||
coreModule.controller('AdminListUsersCtrl', AdminListUsersCtrl);
|
26
public/app/features/admin/state/apis.ts
Normal file
26
public/app/features/admin/state/apis.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
export interface ServerStat {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const getServerStats = async (): Promise<ServerStat[]> => {
|
||||
try {
|
||||
const res = await getBackendSrv().get('api/admin/stats');
|
||||
return [
|
||||
{ name: 'Total users', value: res.users },
|
||||
{ name: 'Total dashboards', value: res.dashboards },
|
||||
{ name: 'Active users (seen last 30 days)', value: res.activeUsers },
|
||||
{ name: 'Total orgs', value: res.orgs },
|
||||
{ name: 'Total playlists', value: res.playlists },
|
||||
{ name: 'Total snapshots', value: res.snapshots },
|
||||
{ name: 'Total dashboard tags', value: res.tags },
|
||||
{ name: 'Total starred dashboards', value: res.stars },
|
||||
{ name: 'Total alerts', value: res.alerts },
|
||||
];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
38
public/app/features/alerting/AlertRuleItem.test.tsx
Normal file
38
public/app/features/alerting/AlertRuleItem.test.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import AlertRuleItem, { Props } from './AlertRuleItem';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
connect: () => params => params,
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
rule: {
|
||||
id: 1,
|
||||
dashboardId: 1,
|
||||
panelId: 1,
|
||||
name: 'Some rule',
|
||||
state: 'Open',
|
||||
stateText: 'state text',
|
||||
stateIcon: 'icon',
|
||||
stateClass: 'state class',
|
||||
stateAge: 'age',
|
||||
url: 'https://something.something.darkside',
|
||||
},
|
||||
search: '',
|
||||
onTogglePause: jest.fn(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<AlertRuleItem {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
69
public/app/features/alerting/AlertRuleItem.tsx
Normal file
69
public/app/features/alerting/AlertRuleItem.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import classNames from 'classnames/bind';
|
||||
import { AlertRule } from '../../types';
|
||||
|
||||
export interface Props {
|
||||
rule: AlertRule;
|
||||
search: string;
|
||||
onTogglePause: () => void;
|
||||
}
|
||||
|
||||
class AlertRuleItem extends PureComponent<Props> {
|
||||
renderText(text: string) {
|
||||
return (
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
textToHighlight={text}
|
||||
searchWords={[this.props.search]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { rule, onTogglePause } = this.props;
|
||||
|
||||
const stateClass = classNames({
|
||||
fa: true,
|
||||
'fa-play': rule.state === 'paused',
|
||||
'fa-pause': rule.state !== 'paused',
|
||||
});
|
||||
|
||||
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
|
||||
|
||||
return (
|
||||
<li className="alert-rule-item">
|
||||
<span className={`alert-rule-item__icon ${rule.stateClass}`}>
|
||||
<i className={rule.stateIcon} />
|
||||
</span>
|
||||
<div className="alert-rule-item__body">
|
||||
<div className="alert-rule-item__header">
|
||||
<div className="alert-rule-item__name">
|
||||
<a href={ruleUrl}>{this.renderText(rule.name)}</a>
|
||||
</div>
|
||||
<div className="alert-rule-item__text">
|
||||
<span className={`${rule.stateClass}`}>{this.renderText(rule.stateText)}</span>
|
||||
<span className="alert-rule-item__time"> for {rule.stateAge}</span>
|
||||
</div>
|
||||
</div>
|
||||
{rule.info && <div className="small muted alert-rule-item__info">{this.renderText(rule.info)}</div>}
|
||||
</div>
|
||||
|
||||
<div className="alert-rule-item__actions">
|
||||
<button
|
||||
className="btn btn-small btn-inverse alert-list__btn width-2"
|
||||
title="Pausing an alert rule prevents it from executing"
|
||||
onClick={onTogglePause}
|
||||
>
|
||||
<i className={stateClass} />
|
||||
</button>
|
||||
<a className="btn btn-small btn-inverse alert-list__btn width-2" href={ruleUrl} title="Edit alert rule">
|
||||
<i className="icon-gf icon-gf-settings" />
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AlertRuleItem;
|
156
public/app/features/alerting/AlertRuleList.test.tsx
Normal file
156
public/app/features/alerting/AlertRuleList.test.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { AlertRuleList, Props } from './AlertRuleList';
|
||||
import { AlertRule, NavModel } from '../../types';
|
||||
import appEvents from '../../core/app_events';
|
||||
|
||||
jest.mock('../../core/app_events', () => ({
|
||||
emit: jest.fn(),
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
navModel: {} as NavModel,
|
||||
alertRules: [] as AlertRule[],
|
||||
updateLocation: jest.fn(),
|
||||
getAlertRulesAsync: jest.fn(),
|
||||
setSearchQuery: jest.fn(),
|
||||
togglePauseAlertRule: jest.fn(),
|
||||
stateFilter: '',
|
||||
search: '',
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<AlertRuleList {...props} />);
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
instance: wrapper.instance() as AlertRuleList,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const { wrapper } = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render alert rules', () => {
|
||||
const { wrapper } = setup({
|
||||
alertRules: [
|
||||
{
|
||||
id: 1,
|
||||
dashboardId: 7,
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
panelId: 3,
|
||||
name: 'TestData - Always OK',
|
||||
state: 'ok',
|
||||
newStateDate: '2018-09-04T10:01:01+02:00',
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
evalData: {},
|
||||
executionError: '',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
dashboardId: 7,
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
panelId: 3,
|
||||
name: 'TestData - ok',
|
||||
state: 'ok',
|
||||
newStateDate: '2018-09-04T10:01:01+02:00',
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
evalData: {},
|
||||
executionError: 'error',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Life cycle', () => {
|
||||
describe('component did mount', () => {
|
||||
it('should call fetchrules', () => {
|
||||
const { instance } = setup();
|
||||
instance.fetchRules = jest.fn();
|
||||
instance.componentDidMount();
|
||||
expect(instance.fetchRules).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component did update', () => {
|
||||
it('should call fetchrules if props differ', () => {
|
||||
const { instance } = setup();
|
||||
instance.fetchRules = jest.fn();
|
||||
|
||||
instance.componentDidUpdate({ stateFilter: 'ok' } as Props);
|
||||
|
||||
expect(instance.fetchRules).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Functions', () => {
|
||||
describe('Get state filter', () => {
|
||||
it('should get all if prop is not set', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
const stateFilter = instance.getStateFilter();
|
||||
|
||||
expect(stateFilter).toEqual('all');
|
||||
});
|
||||
|
||||
it('should return state filter if set', () => {
|
||||
const { instance } = setup({
|
||||
stateFilter: 'ok',
|
||||
});
|
||||
|
||||
const stateFilter = instance.getStateFilter();
|
||||
|
||||
expect(stateFilter).toEqual('ok');
|
||||
});
|
||||
});
|
||||
|
||||
describe('State filter changed', () => {
|
||||
it('should update location', () => {
|
||||
const { instance } = setup();
|
||||
const mockEvent = { target: { value: 'alerting' } };
|
||||
|
||||
instance.onStateFilterChanged(mockEvent);
|
||||
|
||||
expect(instance.props.updateLocation).toHaveBeenCalledWith({ query: { state: 'alerting' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Open how to', () => {
|
||||
it('should emit show-modal event', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
instance.onOpenHowTo();
|
||||
|
||||
expect(appEvents.emit).toHaveBeenCalledWith('show-modal', {
|
||||
src: 'public/app/features/alerting/partials/alert_howto.html',
|
||||
modalClass: 'confirm-modal',
|
||||
model: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search query change', () => {
|
||||
it('should set search query', () => {
|
||||
const { instance } = setup();
|
||||
const mockEvent = { target: { value: 'dashboard' } };
|
||||
|
||||
instance.onSearchQueryChange(mockEvent);
|
||||
|
||||
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('dashboard');
|
||||
});
|
||||
});
|
||||
});
|
153
public/app/features/alerting/AlertRuleList.tsx
Normal file
153
public/app/features/alerting/AlertRuleList.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import AlertRuleItem from './AlertRuleItem';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { NavModel, StoreState, AlertRule } from 'app/types';
|
||||
import { getAlertRulesAsync, setSearchQuery, togglePauseAlertRule } from './state/actions';
|
||||
import { getAlertRuleItems, getSearchQuery } from './state/selectors';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
alertRules: AlertRule[];
|
||||
updateLocation: typeof updateLocation;
|
||||
getAlertRulesAsync: typeof getAlertRulesAsync;
|
||||
setSearchQuery: typeof setSearchQuery;
|
||||
togglePauseAlertRule: typeof togglePauseAlertRule;
|
||||
stateFilter: string;
|
||||
search: string;
|
||||
}
|
||||
|
||||
export class AlertRuleList extends PureComponent<Props, 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' },
|
||||
];
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchRules();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.stateFilter !== this.props.stateFilter) {
|
||||
this.fetchRules();
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRules() {
|
||||
await this.props.getAlertRulesAsync({ state: this.getStateFilter() });
|
||||
}
|
||||
|
||||
getStateFilter(): string {
|
||||
const { stateFilter } = this.props;
|
||||
if (stateFilter) {
|
||||
return stateFilter.toString();
|
||||
}
|
||||
return 'all';
|
||||
}
|
||||
|
||||
onStateFilterChanged = event => {
|
||||
this.props.updateLocation({
|
||||
query: { state: event.target.value },
|
||||
});
|
||||
};
|
||||
|
||||
onOpenHowTo = () => {
|
||||
appEvents.emit('show-modal', {
|
||||
src: 'public/app/features/alerting/partials/alert_howto.html',
|
||||
modalClass: 'confirm-modal',
|
||||
model: {},
|
||||
});
|
||||
};
|
||||
|
||||
onSearchQueryChange = event => {
|
||||
const { value } = event.target;
|
||||
this.props.setSearchQuery(value);
|
||||
};
|
||||
|
||||
onTogglePause = (rule: AlertRule) => {
|
||||
this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' });
|
||||
};
|
||||
|
||||
alertStateFilterOption = ({ text, value }) => {
|
||||
return (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navModel, alertRules, search } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={navModel} />
|
||||
<div className="page-container page-body">
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon gf-form--grow">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search alerts"
|
||||
value={search}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label">States</label>
|
||||
|
||||
<div className="gf-form-select-wrapper width-13">
|
||||
<select className="gf-form-input" onChange={this.onStateFilterChanged} value={this.getStateFilter()}>
|
||||
{this.stateFilters.map(this.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>
|
||||
<ol className="alert-rule-list">
|
||||
{alertRules.map(rule => (
|
||||
<AlertRuleItem
|
||||
rule={rule}
|
||||
key={rule.id}
|
||||
search={search}
|
||||
onTogglePause={() => this.onTogglePause(rule)}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
navModel: getNavModel(state.navIndex, 'alert-list'),
|
||||
alertRules: getAlertRuleItems(state.alertRules),
|
||||
stateFilter: state.location.query.state,
|
||||
search: getSearchQuery(state.alertRules),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateLocation,
|
||||
getAlertRulesAsync,
|
||||
setSearchQuery,
|
||||
togglePauseAlertRule,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList));
|
@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { ThresholdMapper } from './threshold_mapper';
|
||||
import { ThresholdMapper } from './state/ThresholdMapper';
|
||||
import { QueryPart } from 'app/core/components/query_part/query_part';
|
||||
import alertDef from './alert_def';
|
||||
import alertDef from './state/alertDef';
|
||||
import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
@ -1,14 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AlertRuleList should render 1 rule 1`] = `
|
||||
exports[`Render should render component 1`] = `
|
||||
<li
|
||||
className="alert-rule-item"
|
||||
>
|
||||
<span
|
||||
className="alert-rule-item__icon alert-state-ok"
|
||||
className="alert-rule-item__icon state class"
|
||||
>
|
||||
<i
|
||||
className="icon-gf icon-gf-online"
|
||||
className="icon"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
@ -21,7 +21,7 @@ exports[`AlertRuleList should render 1 rule 1`] = `
|
||||
className="alert-rule-item__name"
|
||||
>
|
||||
<a
|
||||
href="d/ufkcofof/my-goal?panelId=3&fullscreen=true&edit=true&tab=alert"
|
||||
href="https://something.something.darkside?panelId=1&fullscreen=true&edit=true&tab=alert"
|
||||
>
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
@ -30,24 +30,15 @@ exports[`AlertRuleList should render 1 rule 1`] = `
|
||||
"",
|
||||
]
|
||||
}
|
||||
textToHighlight="Panel Title alert"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
className=""
|
||||
key="0"
|
||||
>
|
||||
Panel Title alert
|
||||
</span>
|
||||
</span>
|
||||
</Highlighter>
|
||||
textToHighlight="Some rule"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="alert-rule-item__text"
|
||||
>
|
||||
<span
|
||||
className="alert-state-ok"
|
||||
className="state class"
|
||||
>
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
@ -56,23 +47,14 @@ exports[`AlertRuleList should render 1 rule 1`] = `
|
||||
"",
|
||||
]
|
||||
}
|
||||
textToHighlight="OK"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
className=""
|
||||
key="0"
|
||||
>
|
||||
OK
|
||||
</span>
|
||||
</span>
|
||||
</Highlighter>
|
||||
textToHighlight="state text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="alert-rule-item__time"
|
||||
>
|
||||
for
|
||||
5 minutes
|
||||
age
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -82,7 +64,7 @@ exports[`AlertRuleList should render 1 rule 1`] = `
|
||||
>
|
||||
<button
|
||||
className="btn btn-small btn-inverse alert-list__btn width-2"
|
||||
onClick={[Function]}
|
||||
onClick={[MockFunction]}
|
||||
title="Pausing an alert rule prevents it from executing"
|
||||
>
|
||||
<i
|
||||
@ -91,7 +73,7 @@ exports[`AlertRuleList should render 1 rule 1`] = `
|
||||
</button>
|
||||
<a
|
||||
className="btn btn-small btn-inverse alert-list__btn width-2"
|
||||
href="d/ufkcofof/my-goal?panelId=3&fullscreen=true&edit=true&tab=alert"
|
||||
href="https://something.something.darkside?panelId=1&fullscreen=true&edit=true&tab=alert"
|
||||
title="Edit alert rule"
|
||||
>
|
||||
<i
|
@ -0,0 +1,256 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render alert rules 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon gf-form--grow"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="Search alerts"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<label
|
||||
className="gf-form-label"
|
||||
>
|
||||
States
|
||||
</label>
|
||||
<div
|
||||
className="gf-form-select-wrapper width-13"
|
||||
>
|
||||
<select
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
value="all"
|
||||
>
|
||||
<option
|
||||
key="all"
|
||||
value="all"
|
||||
>
|
||||
All
|
||||
</option>
|
||||
<option
|
||||
key="ok"
|
||||
value="ok"
|
||||
>
|
||||
OK
|
||||
</option>
|
||||
<option
|
||||
key="not_ok"
|
||||
value="not_ok"
|
||||
>
|
||||
Not OK
|
||||
</option>
|
||||
<option
|
||||
key="alerting"
|
||||
value="alerting"
|
||||
>
|
||||
Alerting
|
||||
</option>
|
||||
<option
|
||||
key="no_data"
|
||||
value="no_data"
|
||||
>
|
||||
No Data
|
||||
</option>
|
||||
<option
|
||||
key="paused"
|
||||
value="paused"
|
||||
>
|
||||
Paused
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<a
|
||||
className="btn btn-secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-info-circle"
|
||||
/>
|
||||
How to add an alert
|
||||
</a>
|
||||
</div>
|
||||
<section>
|
||||
<ol
|
||||
className="alert-rule-list"
|
||||
>
|
||||
<AlertRuleItem
|
||||
key="1"
|
||||
onTogglePause={[Function]}
|
||||
rule={
|
||||
Object {
|
||||
"dashboardId": 7,
|
||||
"dashboardSlug": "alerting-with-testdata",
|
||||
"dashboardUid": "ggHbN42mk",
|
||||
"evalData": Object {},
|
||||
"evalDate": "0001-01-01T00:00:00Z",
|
||||
"executionError": "",
|
||||
"id": 1,
|
||||
"name": "TestData - Always OK",
|
||||
"newStateDate": "2018-09-04T10:01:01+02:00",
|
||||
"panelId": 3,
|
||||
"state": "ok",
|
||||
"url": "/d/ggHbN42mk/alerting-with-testdata",
|
||||
}
|
||||
}
|
||||
search=""
|
||||
/>
|
||||
<AlertRuleItem
|
||||
key="3"
|
||||
onTogglePause={[Function]}
|
||||
rule={
|
||||
Object {
|
||||
"dashboardId": 7,
|
||||
"dashboardSlug": "alerting-with-testdata",
|
||||
"dashboardUid": "ggHbN42mk",
|
||||
"evalData": Object {},
|
||||
"evalDate": "0001-01-01T00:00:00Z",
|
||||
"executionError": "error",
|
||||
"id": 3,
|
||||
"name": "TestData - ok",
|
||||
"newStateDate": "2018-09-04T10:01:01+02:00",
|
||||
"panelId": 3,
|
||||
"state": "ok",
|
||||
"url": "/d/ggHbN42mk/alerting-with-testdata",
|
||||
}
|
||||
}
|
||||
search=""
|
||||
/>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<div
|
||||
className="page-action-bar"
|
||||
>
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon gf-form--grow"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="Search alerts"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<label
|
||||
className="gf-form-label"
|
||||
>
|
||||
States
|
||||
</label>
|
||||
<div
|
||||
className="gf-form-select-wrapper width-13"
|
||||
>
|
||||
<select
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
value="all"
|
||||
>
|
||||
<option
|
||||
key="all"
|
||||
value="all"
|
||||
>
|
||||
All
|
||||
</option>
|
||||
<option
|
||||
key="ok"
|
||||
value="ok"
|
||||
>
|
||||
OK
|
||||
</option>
|
||||
<option
|
||||
key="not_ok"
|
||||
value="not_ok"
|
||||
>
|
||||
Not OK
|
||||
</option>
|
||||
<option
|
||||
key="alerting"
|
||||
value="alerting"
|
||||
>
|
||||
Alerting
|
||||
</option>
|
||||
<option
|
||||
key="no_data"
|
||||
value="no_data"
|
||||
>
|
||||
No Data
|
||||
</option>
|
||||
<option
|
||||
key="paused"
|
||||
value="paused"
|
||||
>
|
||||
Paused
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="page-action-bar__spacer"
|
||||
/>
|
||||
<a
|
||||
className="btn btn-secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-info-circle"
|
||||
/>
|
||||
How to add an alert
|
||||
</a>
|
||||
</div>
|
||||
<section>
|
||||
<ol
|
||||
className="alert-rule-list"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,2 +0,0 @@
|
||||
import './notifications_list_ctrl';
|
||||
import './notification_edit_ctrl';
|
@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'test/lib/common';
|
||||
|
||||
import { ThresholdMapper } from '../threshold_mapper';
|
||||
import { ThresholdMapper } from './ThresholdMapper';
|
||||
|
||||
describe('ThresholdMapper', () => {
|
||||
describe('with greater than evaluator', () => {
|
47
public/app/features/alerting/state/actions.ts
Normal file
47
public/app/features/alerting/state/actions.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { AlertRuleApi, StoreState } from 'app/types';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadAlertRules = 'LOAD_ALERT_RULES',
|
||||
SetSearchQuery = 'SET_SEARCH_QUERY',
|
||||
}
|
||||
|
||||
export interface LoadAlertRulesAction {
|
||||
type: ActionTypes.LoadAlertRules;
|
||||
payload: AlertRuleApi[];
|
||||
}
|
||||
|
||||
export interface SetSearchQueryAction {
|
||||
type: ActionTypes.SetSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export const loadAlertRules = (rules: AlertRuleApi[]): LoadAlertRulesAction => ({
|
||||
type: ActionTypes.LoadAlertRules,
|
||||
payload: rules,
|
||||
});
|
||||
|
||||
export const setSearchQuery = (query: string): SetSearchQueryAction => ({
|
||||
type: ActionTypes.SetSearchQuery,
|
||||
payload: query,
|
||||
});
|
||||
|
||||
export type Action = LoadAlertRulesAction | SetSearchQueryAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||
|
||||
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const rules = await getBackendSrv().get('/api/alerts', options);
|
||||
dispatch(loadAlertRules(rules));
|
||||
};
|
||||
}
|
||||
|
||||
export function togglePauseAlertRule(id: number, options: { paused: boolean }): ThunkResult<void> {
|
||||
return async (dispatch, getState) => {
|
||||
await getBackendSrv().post(`/api/alerts/${id}/pause`, options);
|
||||
const stateFilter = getState().location.query.state || 'all';
|
||||
dispatch(getAlertRulesAsync({ state: stateFilter.toString() }));
|
||||
};
|
||||
}
|
91
public/app/features/alerting/state/reducers.test.ts
Normal file
91
public/app/features/alerting/state/reducers.test.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { ActionTypes, Action } from './actions';
|
||||
import { alertRulesReducer, initialState } from './reducers';
|
||||
import { AlertRuleApi } from '../../../types';
|
||||
|
||||
describe('Alert rules', () => {
|
||||
const payload: AlertRuleApi[] = [
|
||||
{
|
||||
id: 2,
|
||||
dashboardId: 7,
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
panelId: 4,
|
||||
name: 'TestData - Always Alerting',
|
||||
state: 'alerting',
|
||||
newStateDate: '2018-09-04T10:00:30+02:00',
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
evalData: { evalMatches: [{ metric: 'A-series', tags: null, value: 215 }] },
|
||||
executionError: '',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
dashboardId: 7,
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
panelId: 3,
|
||||
name: 'TestData - Always OK',
|
||||
state: 'ok',
|
||||
newStateDate: '2018-09-04T10:01:01+02:00',
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
evalData: {},
|
||||
executionError: '',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
dashboardId: 7,
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
panelId: 3,
|
||||
name: 'TestData - ok',
|
||||
state: 'ok',
|
||||
newStateDate: '2018-09-04T10:01:01+02:00',
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
evalData: {},
|
||||
executionError: 'error',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
dashboardId: 7,
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
panelId: 3,
|
||||
name: 'TestData - Paused',
|
||||
state: 'paused',
|
||||
newStateDate: '2018-09-04T10:01:01+02:00',
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
evalData: {},
|
||||
executionError: 'error',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
dashboardId: 7,
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
panelId: 3,
|
||||
name: 'TestData - Ok',
|
||||
state: 'ok',
|
||||
newStateDate: '2018-09-04T10:01:01+02:00',
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
evalData: {
|
||||
noData: true,
|
||||
},
|
||||
executionError: 'error',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
];
|
||||
|
||||
it('should set alert rules', () => {
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadAlertRules,
|
||||
payload: payload,
|
||||
};
|
||||
|
||||
const result = alertRulesReducer(initialState, action);
|
||||
|
||||
expect(result.items).toEqual(payload);
|
||||
});
|
||||
});
|
50
public/app/features/alerting/state/reducers.ts
Normal file
50
public/app/features/alerting/state/reducers.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import moment from 'moment';
|
||||
import { AlertRuleApi, AlertRule, AlertRulesState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import alertDef from './alertDef';
|
||||
|
||||
export const initialState: AlertRulesState = { items: [], searchQuery: '' };
|
||||
|
||||
function convertToAlertRule(rule, state): AlertRule {
|
||||
const stateModel = alertDef.getStateDisplayModel(state);
|
||||
rule.stateText = stateModel.text;
|
||||
rule.stateIcon = stateModel.iconClass;
|
||||
rule.stateClass = stateModel.stateClass;
|
||||
rule.stateAge = moment(rule.newStateDate)
|
||||
.fromNow()
|
||||
.replace(' ago', '');
|
||||
|
||||
if (rule.state !== 'paused') {
|
||||
if (rule.executionError) {
|
||||
rule.info = 'Execution Error: ' + rule.executionError;
|
||||
}
|
||||
if (rule.evalData && rule.evalData.noData) {
|
||||
rule.info = 'Query returned no data';
|
||||
}
|
||||
}
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadAlertRules: {
|
||||
const alertRules: AlertRuleApi[] = action.payload;
|
||||
|
||||
const alertRulesViewModel: AlertRule[] = alertRules.map(rule => {
|
||||
return convertToAlertRule(rule, rule.state);
|
||||
});
|
||||
|
||||
return { items: alertRulesViewModel, searchQuery: state.searchQuery };
|
||||
}
|
||||
|
||||
case ActionTypes.SetSearchQuery:
|
||||
return { items: state.items, searchQuery: action.payload };
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default {
|
||||
alertRules: alertRulesReducer,
|
||||
};
|
94
public/app/features/alerting/state/selectors.test.ts
Normal file
94
public/app/features/alerting/state/selectors.test.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { getSearchQuery, getAlertRuleItems } from './selectors';
|
||||
|
||||
describe('Get search query', () => {
|
||||
it('should get search query', () => {
|
||||
const state = { searchQuery: 'dashboard' };
|
||||
const result = getSearchQuery(state);
|
||||
|
||||
expect(result).toEqual(state.searchQuery);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get alert rule items', () => {
|
||||
it('should get alert rule items', () => {
|
||||
const state = {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
dashboardId: 1,
|
||||
panelId: 1,
|
||||
name: '',
|
||||
state: '',
|
||||
stateText: '',
|
||||
stateIcon: '',
|
||||
stateClass: '',
|
||||
stateAge: '',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
const result = getAlertRuleItems(state);
|
||||
expect(result.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should filter rule items based on search query', () => {
|
||||
const state = {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
dashboardId: 1,
|
||||
panelId: 1,
|
||||
name: 'dashboard',
|
||||
state: '',
|
||||
stateText: '',
|
||||
stateIcon: '',
|
||||
stateClass: '',
|
||||
stateAge: '',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
dashboardId: 3,
|
||||
panelId: 1,
|
||||
name: 'dashboard2',
|
||||
state: '',
|
||||
stateText: '',
|
||||
stateIcon: '',
|
||||
stateClass: '',
|
||||
stateAge: '',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
dashboardId: 5,
|
||||
panelId: 1,
|
||||
name: 'hello',
|
||||
state: '',
|
||||
stateText: '',
|
||||
stateIcon: '',
|
||||
stateClass: '',
|
||||
stateAge: '',
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
dashboardId: 7,
|
||||
panelId: 1,
|
||||
name: 'test',
|
||||
state: '',
|
||||
stateText: 'dashboard',
|
||||
stateIcon: '',
|
||||
stateClass: '',
|
||||
stateAge: '',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
searchQuery: 'dashboard',
|
||||
};
|
||||
|
||||
const result = getAlertRuleItems(state);
|
||||
expect(result.length).toEqual(3);
|
||||
});
|
||||
});
|
9
public/app/features/alerting/state/selectors.ts
Normal file
9
public/app/features/alerting/state/selectors.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const getSearchQuery = state => state.searchQuery;
|
||||
|
||||
export const getAlertRuleItems = state => {
|
||||
const regex = new RegExp(state.searchQuery, 'i');
|
||||
|
||||
return state.items.filter(item => {
|
||||
return regex.test(item.name) || regex.test(item.stateText) || regex.test(item.info);
|
||||
});
|
||||
};
|
@ -8,6 +8,7 @@ import './playlist/all';
|
||||
import './snapshot/all';
|
||||
import './panel/all';
|
||||
import './org/all';
|
||||
import './admin/admin';
|
||||
import './alerting/all';
|
||||
import './admin';
|
||||
import './alerting/NotificationsEditCtrl';
|
||||
import './alerting/NotificationsListCtrl';
|
||||
import './styleguide/styleguide';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import alertDef from '../alerting/alert_def';
|
||||
import alertDef from '../alerting/state/alertDef';
|
||||
|
||||
/** @ngInject */
|
||||
export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, $compile) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import alertDef from '../../../features/alerting/alert_def';
|
||||
import alertDef from '../../../features/alerting/state/alertDef';
|
||||
import { PanelCtrl } from 'app/plugins/sdk';
|
||||
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { PanelCtrl } from 'app/features/panel/panel_ctrl';
|
||||
import { MetricsPanelCtrl } from 'app/features/panel/metrics_panel_ctrl';
|
||||
import { QueryCtrl } from 'app/features/panel/query_ctrl';
|
||||
import { alertTab } from 'app/features/alerting/alert_tab_ctrl';
|
||||
import { alertTab } from 'app/features/alerting/AlertTabCtrl';
|
||||
import { loadPluginCss } from 'app/features/plugins/plugin_loader';
|
||||
|
||||
export { PanelCtrl, MetricsPanelCtrl, QueryCtrl, alertTab, loadPluginCss };
|
||||
|
@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'mobx-react';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { store } from 'app/stores/store';
|
||||
import { store as reduxStore } from 'app/stores/configureStore';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
function WrapInProvider(store, Component, props) {
|
||||
return (
|
||||
<Provider {...store}>
|
||||
<Component {...props} />
|
||||
</Provider>
|
||||
<ReduxProvider store={reduxStore}>
|
||||
<Provider {...store}>
|
||||
<Component {...props} />
|
||||
</Provider>
|
||||
</ReduxProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import './dashboard_loaders';
|
||||
import './ReactContainer';
|
||||
|
||||
import ServerStats from 'app/containers/ServerStats/ServerStats';
|
||||
import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList';
|
||||
import ServerStats from 'app/features/admin/ServerStats';
|
||||
import AlertRuleList from 'app/features/alerting/AlertRuleList';
|
||||
import FolderSettings from 'app/containers/ManageDashboards/FolderSettings';
|
||||
import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
|
||||
import TeamPages from 'app/containers/Teams/TeamPages';
|
||||
|
@ -1,66 +0,0 @@
|
||||
import { AlertListStore } from './AlertListStore';
|
||||
import { backendSrv } from 'test/mocks/common';
|
||||
import moment from 'moment';
|
||||
|
||||
function getRule(name, state, info) {
|
||||
return {
|
||||
id: 11,
|
||||
dashboardId: 58,
|
||||
panelId: 3,
|
||||
name: name,
|
||||
state: state,
|
||||
newStateDate: moment()
|
||||
.subtract(5, 'minutes')
|
||||
.format(),
|
||||
evalData: {},
|
||||
executionError: '',
|
||||
url: 'db/mygool',
|
||||
stateText: state,
|
||||
stateIcon: 'fa',
|
||||
stateClass: 'asd',
|
||||
stateAge: '10m',
|
||||
info: info,
|
||||
canEdit: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe('AlertListStore', () => {
|
||||
let store;
|
||||
|
||||
beforeAll(() => {
|
||||
store = AlertListStore.create(
|
||||
{
|
||||
rules: [
|
||||
getRule('Europe', 'OK', 'backend-01'),
|
||||
getRule('Google', 'ALERTING', 'backend-02'),
|
||||
getRule('Amazon', 'PAUSED', 'backend-03'),
|
||||
getRule('West-Europe', 'PAUSED', 'backend-03'),
|
||||
],
|
||||
search: '',
|
||||
},
|
||||
{
|
||||
backendSrv: backendSrv,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('search should filter list on name', () => {
|
||||
store.setSearchQuery('urope');
|
||||
expect(store.filteredRules).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('search should filter list on state', () => {
|
||||
store.setSearchQuery('ale');
|
||||
expect(store.filteredRules).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('search should filter list on info', () => {
|
||||
store.setSearchQuery('-0');
|
||||
expect(store.filteredRules).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('search should be equal', () => {
|
||||
store.setSearchQuery('alert');
|
||||
expect(store.search).toBe('alert');
|
||||
});
|
||||
});
|
@ -1,47 +0,0 @@
|
||||
import { types, getEnv, flow } from 'mobx-state-tree';
|
||||
import { AlertRule as AlertRuleModel } from './AlertRule';
|
||||
import { setStateFields } from './helpers';
|
||||
|
||||
type AlertRuleType = typeof AlertRuleModel.Type;
|
||||
export interface AlertRule extends AlertRuleType {}
|
||||
|
||||
export const AlertListStore = types
|
||||
.model('AlertListStore', {
|
||||
rules: types.array(AlertRuleModel),
|
||||
stateFilter: types.optional(types.string, 'all'),
|
||||
search: types.optional(types.string, ''),
|
||||
})
|
||||
.views(self => ({
|
||||
get filteredRules() {
|
||||
const regex = new RegExp(self.search, 'i');
|
||||
return self.rules.filter(alert => {
|
||||
return regex.test(alert.name) || regex.test(alert.stateText) || regex.test(alert.info);
|
||||
});
|
||||
},
|
||||
}))
|
||||
.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 (const rule of apiRules) {
|
||||
setStateFields(rule, rule.state);
|
||||
|
||||
if (rule.state !== 'paused') {
|
||||
if (rule.executionError) {
|
||||
rule.info = 'Execution Error: ' + rule.executionError;
|
||||
}
|
||||
if (rule.evalData && rule.evalData.noData) {
|
||||
rule.info = 'Query returned no data';
|
||||
}
|
||||
}
|
||||
|
||||
self.rules.push(AlertRuleModel.create(rule));
|
||||
}
|
||||
}),
|
||||
setSearchQuery(query: string) {
|
||||
self.search = query;
|
||||
},
|
||||
}));
|
@ -1,34 +0,0 @@
|
||||
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, ''),
|
||||
url: 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 = '';
|
||||
}),
|
||||
}));
|
@ -1,13 +0,0 @@
|
||||
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', '');
|
||||
}
|
@ -1,24 +1,12 @@
|
||||
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';
|
||||
import { FolderStore } from './../FolderStore/FolderStore';
|
||||
import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
|
||||
import { TeamsStore } from './../TeamsStore/TeamsStore';
|
||||
|
||||
export const RootStore = types.model({
|
||||
search: types.optional(SearchStore, {
|
||||
sections: [],
|
||||
}),
|
||||
serverStats: types.optional(ServerStatsStore, {
|
||||
stats: [],
|
||||
}),
|
||||
nav: types.optional(NavStore, {}),
|
||||
alertList: types.optional(AlertListStore, {
|
||||
rules: [],
|
||||
}),
|
||||
permissions: types.optional(PermissionsStore, {
|
||||
fetching: false,
|
||||
items: [],
|
||||
|
@ -1,10 +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, ''),
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
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',
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
}));
|
@ -1,22 +0,0 @@
|
||||
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: [],
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
}));
|
@ -1,6 +0,0 @@
|
||||
import { types } from 'mobx-state-tree';
|
||||
|
||||
export const ServerStat = types.model('ServerStat', {
|
||||
name: types.string,
|
||||
value: types.optional(types.number, 0),
|
||||
});
|
@ -1,24 +0,0 @@
|
||||
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 }));
|
||||
}),
|
||||
}));
|
23
public/app/stores/configureStore.ts
Normal file
23
public/app/stores/configureStore.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import sharedReducers from 'app/core/reducers';
|
||||
import alertingReducers from 'app/features/alerting/state/reducers';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
...sharedReducers,
|
||||
...alertingReducers,
|
||||
});
|
||||
|
||||
export let store;
|
||||
|
||||
export function configureStore() {
|
||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// DEV builds we had the logger middleware
|
||||
store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())));
|
||||
} else {
|
||||
store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)));
|
||||
}
|
||||
}
|
96
public/app/types/index.ts
Normal file
96
public/app/types/index.ts
Normal file
@ -0,0 +1,96 @@
|
||||
//
|
||||
// Location
|
||||
//
|
||||
|
||||
export interface LocationUpdate {
|
||||
path?: string;
|
||||
query?: UrlQueryMap;
|
||||
routeParams?: UrlQueryMap;
|
||||
}
|
||||
|
||||
export interface LocationState {
|
||||
url: string;
|
||||
path: string;
|
||||
query: UrlQueryMap;
|
||||
routeParams: UrlQueryMap;
|
||||
}
|
||||
|
||||
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[];
|
||||
export type UrlQueryMap = { [s: string]: UrlQueryValue };
|
||||
|
||||
//
|
||||
// Alerting
|
||||
//
|
||||
|
||||
export interface AlertRuleApi {
|
||||
id: number;
|
||||
dashboardId: number;
|
||||
dashboardUid: string;
|
||||
dashboardSlug: string;
|
||||
panelId: number;
|
||||
name: string;
|
||||
state: string;
|
||||
newStateDate: string;
|
||||
evalDate: string;
|
||||
evalData?: object;
|
||||
executionError: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface AlertRule {
|
||||
id: number;
|
||||
dashboardId: number;
|
||||
panelId: number;
|
||||
name: string;
|
||||
state: string;
|
||||
stateText: string;
|
||||
stateIcon: string;
|
||||
stateClass: string;
|
||||
stateAge: string;
|
||||
url: string;
|
||||
info?: string;
|
||||
executionError?: string;
|
||||
evalData?: { noData: boolean };
|
||||
}
|
||||
|
||||
//
|
||||
// NavModel
|
||||
//
|
||||
|
||||
export interface NavModelItem {
|
||||
text: string;
|
||||
url: string;
|
||||
subTitle?: string;
|
||||
icon?: string;
|
||||
img?: string;
|
||||
id: string;
|
||||
active?: boolean;
|
||||
hideFromTabs?: boolean;
|
||||
divider?: boolean;
|
||||
children?: NavModelItem[];
|
||||
breadcrumbs?: NavModelItem[];
|
||||
target?: string;
|
||||
parentItem?: NavModelItem;
|
||||
}
|
||||
|
||||
export interface NavModel {
|
||||
main: NavModelItem;
|
||||
node: NavModelItem;
|
||||
}
|
||||
|
||||
export type NavIndex = { [s: string]: NavModelItem };
|
||||
|
||||
//
|
||||
// Store
|
||||
//
|
||||
|
||||
export interface AlertRulesState {
|
||||
items: AlertRule[];
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
export interface StoreState {
|
||||
navIndex: NavIndex;
|
||||
location: LocationState;
|
||||
alertRules: AlertRulesState;
|
||||
}
|
@ -20,3 +20,24 @@ configure({ adapter: new Adapter() });
|
||||
|
||||
const global = window as any;
|
||||
global.$ = global.jQuery = $;
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store = {};
|
||||
return {
|
||||
getItem: key => {
|
||||
return store[key];
|
||||
},
|
||||
setItem: (key, value) => {
|
||||
store[key] = value.toString();
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
removeItem: key => {
|
||||
delete store[key];
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
global.localStorage = localStorageMock;
|
||||
// Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { NavModel, NavModelItem } from 'app/types';
|
||||
|
||||
export const backendSrv = {
|
||||
get: jest.fn(),
|
||||
getDashboard: jest.fn(),
|
||||
@ -17,3 +19,33 @@ export function createNavTree(...args) {
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
export function createNavModel(title: string, ...tabs: string[]): NavModel {
|
||||
const node: NavModelItem = {
|
||||
id: title,
|
||||
text: title,
|
||||
icon: 'fa fa-fw fa-warning',
|
||||
subTitle: 'subTitle',
|
||||
url: title,
|
||||
children: [],
|
||||
breadcrumbs: [],
|
||||
};
|
||||
|
||||
for (const tab of tabs) {
|
||||
node.children.push({
|
||||
id: tab,
|
||||
icon: 'icon',
|
||||
subTitle: 'subTitle',
|
||||
url: title,
|
||||
text: title,
|
||||
active: false,
|
||||
});
|
||||
}
|
||||
|
||||
node.children[0].active = true;
|
||||
|
||||
return {
|
||||
node: node,
|
||||
main: node,
|
||||
};
|
||||
}
|
||||
|
40
yarn.lock
40
yarn.lock
@ -3188,6 +3188,10 @@ dedent@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
|
||||
|
||||
deep-diff@^0.3.5:
|
||||
version "0.3.8"
|
||||
resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84"
|
||||
|
||||
deep-equal@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
||||
@ -5625,7 +5629,7 @@ into-stream@^3.1.0:
|
||||
from2 "^2.1.1"
|
||||
p-is-promise "^1.1.0"
|
||||
|
||||
invariant@^2.2.2:
|
||||
invariant@^2.0.0, invariant@^2.2.2:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
dependencies:
|
||||
@ -6909,6 +6913,10 @@ lockfile@^1.0.4:
|
||||
dependencies:
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
lodash-es@^4.17.5:
|
||||
version "4.17.10"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
|
||||
|
||||
lodash._baseuniq@~4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
|
||||
@ -9610,6 +9618,17 @@ react-reconciler@^0.7.0:
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-redux@^5.0.7:
|
||||
version "5.0.7"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
|
||||
dependencies:
|
||||
hoist-non-react-statics "^2.5.0"
|
||||
invariant "^2.0.0"
|
||||
lodash "^4.17.5"
|
||||
lodash-es "^4.17.5"
|
||||
loose-envify "^1.1.0"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-resizable@1.x:
|
||||
version "1.7.5"
|
||||
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"
|
||||
@ -9864,6 +9883,23 @@ reduce-function-call@^1.0.1:
|
||||
dependencies:
|
||||
balanced-match "^0.4.2"
|
||||
|
||||
redux-logger@^3.0.6:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf"
|
||||
dependencies:
|
||||
deep-diff "^0.3.5"
|
||||
|
||||
redux-thunk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
|
||||
redux@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03"
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
symbol-observable "^1.2.0"
|
||||
|
||||
regenerate@^1.2.1:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
|
||||
@ -11182,7 +11218,7 @@ symbol-observable@^0.2.2:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"
|
||||
|
||||
symbol-observable@^1.1.0:
|
||||
symbol-observable@^1.1.0, symbol-observable@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user