This commit is contained in:
Peter Holmberg 2018-09-04 15:00:04 +02:00
parent 5ac5a08e9e
commit 22510be450
9 changed files with 526 additions and 184 deletions

View File

@ -3,7 +3,7 @@ import { shallow } from 'enzyme';
import AlertRuleItem, { Props } from './AlertRuleItem'; import AlertRuleItem, { Props } from './AlertRuleItem';
jest.mock('react-redux', () => ({ jest.mock('react-redux', () => ({
connect: params => params, connect: () => params => params,
})); }));
const setup = (propOverrides?: object) => { const setup = (propOverrides?: object) => {
@ -23,6 +23,7 @@ const setup = (propOverrides?: object) => {
search: '', search: '',
togglePauseAlertRule: jest.fn(), togglePauseAlertRule: jest.fn(),
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
return shallow(<AlertRuleItem {...props} />); return shallow(<AlertRuleItem {...props} />);

View File

@ -15,7 +15,7 @@ class AlertRuleItem extends PureComponent<Props, any> {
togglePaused = () => { togglePaused = () => {
const { rule } = this.props; const { rule } = this.props;
this.props.togglePauseAlertRule(rule.id, { paused: rule.state === 'paused' }); this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' });
}; };
renderText(text: string) { renderText(text: string) {

View File

@ -1,69 +1,159 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import { shallow } from 'enzyme';
import { AlertRuleList } from './AlertRuleList'; import AlertRuleList, { Props } from './AlertRuleList';
import { RootStore } from 'app/stores/RootStore/RootStore'; import { AlertRule, NavModel } from '../../types';
import { backendSrv, createNavTree } from 'test/mocks/common'; import appEvents from '../../core/app_events';
import { mount } from 'enzyme';
import toJson from 'enzyme-to-json';
describe('AlertRuleList', () => { jest.mock('react-redux', () => ({
let page, store; connect: () => params => params,
}));
beforeAll(() => { jest.mock('../../core/app_events', () => ({
backendSrv.get.mockReturnValue( emit: jest.fn(),
Promise.resolve([ }));
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {} as NavModel,
alertRules: [] as AlertRule[],
updateLocation: jest.fn(),
getAlertRulesAsync: jest.fn(),
setSearchQuery: 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: 11, id: 1,
dashboardId: 58, dashboardId: 7,
dashboardUid: 'ggHbN42mk',
dashboardSlug: 'alerting-with-testdata',
panelId: 3, panelId: 3,
name: 'Panel Title alert', name: 'TestData - Always OK',
state: 'ok', state: 'ok',
newStateDate: moment() newStateDate: '2018-09-04T10:01:01+02:00',
.subtract(5, 'minutes') evalDate: '0001-01-01T00:00:00Z',
.format(),
evalData: {}, evalData: {},
executionError: '', executionError: '',
url: 'd/ufkcofof/my-goal', url: '/d/ggHbN42mk/alerting-with-testdata',
canEdit: true,
}, },
]) {
); 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',
},
],
});
store = RootStore.create( expect(wrapper).toMatchSnapshot();
{}, });
{ });
backendSrv: backendSrv,
navTree: createNavTree('alerting', 'alert-list'), describe('Life cycle', () => {
} describe('component did mount', () => {
); it('should call fetchrules', () => {
const { instance } = setup();
page = mount(<AlertRuleList {...store} />); instance.fetchRules = jest.fn();
}); instance.componentDidMount();
expect(instance.fetchRules).toHaveBeenCalled();
it('should call api to get rules', () => { });
expect(backendSrv.get.mock.calls[0][0]).toEqual('/api/alerts'); });
});
describe('component did update', () => {
it('should render 1 rule', () => { it('should call fetchrules if props differ', () => {
page.update(); const { instance } = setup();
const ruleNode = page.find('.alert-rule-item'); instance.fetchRules = jest.fn();
expect(toJson(ruleNode)).toMatchSnapshot();
}); instance.componentDidUpdate({ stateFilter: 'ok' });
it('toggle state should change pause rule if not paused', async () => { expect(instance.fetchRules).toHaveBeenCalled();
backendSrv.post.mockReturnValue( });
Promise.resolve({ });
state: 'paused', });
})
); describe('Functions', () => {
describe('Get state filter', () => {
page.find('.fa-pause').simulate('click'); it('should get all if prop is not set', () => {
const { instance } = setup();
// wait for api call to resolve
await Promise.resolve(); const stateFilter = instance.getStateFilter();
page.update();
expect(stateFilter).toEqual('all');
expect(store.alertList.rules[0].state).toBe('paused'); });
expect(page.find('.fa-play')).toHaveLength(1);
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');
});
}); });
}); });

View File

@ -10,7 +10,7 @@ import { NavModel, StoreState, AlertRule } from 'app/types';
import { getAlertRulesAsync, setSearchQuery } from './state/actions'; import { getAlertRulesAsync, setSearchQuery } from './state/actions';
import { getAlertRuleItems, getSearchQuery } from './state/selectors'; import { getAlertRuleItems, getSearchQuery } from './state/selectors';
interface Props { export interface Props {
navModel: NavModel; navModel: NavModel;
alertRules: AlertRule[]; alertRules: AlertRule[];
updateLocation: typeof updateLocation; updateLocation: typeof updateLocation;
@ -20,11 +20,7 @@ interface Props {
search: string; search: string;
} }
interface State { class AlertRuleList extends PureComponent<Props, any> {
search: string;
}
export class AlertRuleList extends PureComponent<Props, State> {
stateFilters = [ stateFilters = [
{ text: 'All', value: 'all' }, { text: 'All', value: 'all' },
{ text: 'OK', value: 'ok' }, { text: 'OK', value: 'ok' },
@ -44,11 +40,9 @@ export class AlertRuleList extends PureComponent<Props, State> {
} }
} }
onStateFilterChanged = evt => { async fetchRules() {
this.props.updateLocation({ await this.props.getAlertRulesAsync({ state: this.getStateFilter() });
query: { state: evt.target.value }, }
});
};
getStateFilter(): string { getStateFilter(): string {
const { stateFilter } = this.props; const { stateFilter } = this.props;
@ -58,9 +52,11 @@ export class AlertRuleList extends PureComponent<Props, State> {
return 'all'; return 'all';
} }
async fetchRules() { onStateFilterChanged = event => {
await this.props.getAlertRulesAsync({ state: this.getStateFilter() }); this.props.updateLocation({
} query: { state: event.target.value },
});
};
onOpenHowTo = () => { onOpenHowTo = () => {
appEvents.emit('show-modal', { appEvents.emit('show-modal', {
@ -75,13 +71,13 @@ export class AlertRuleList extends PureComponent<Props, State> {
this.props.setSearchQuery(value); this.props.setSearchQuery(value);
}; };
alertStateFilterOption({ text, value }) { alertStateFilterOption = ({ text, value }) => {
return ( return (
<option key={value} value={value}> <option key={value} value={value}>
{text} {text}
</option> </option>
); );
} };
render() { render() {
const { navModel, alertRules, search } = this.props; const { navModel, alertRules, search } = this.props;
@ -112,14 +108,11 @@ export class AlertRuleList extends PureComponent<Props, State> {
</select> </select>
</div> </div>
</div> </div>
<div className="page-action-bar__spacer" /> <div className="page-action-bar__spacer" />
<a className="btn btn-secondary" onClick={this.onOpenHowTo}> <a className="btn btn-secondary" onClick={this.onOpenHowTo}>
<i className="fa fa-info-circle" /> How to add an alert <i className="fa fa-info-circle" /> How to add an alert
</a> </a>
</div> </div>
<section> <section>
<ol className="alert-rule-list"> <ol className="alert-rule-list">
{alertRules.map(rule => <AlertRuleItem rule={rule} key={rule.id} search={search} />)} {alertRules.map(rule => <AlertRuleItem rule={rule} key={rule.id} search={search} />)}

View File

@ -1,103 +1,254 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertRuleList should render 1 rule 1`] = ` exports[`Render should render alert rules 1`] = `
<li <div>
className="alert-rule-item" <PageHeader
> model={Object {}}
<span />
className="alert-rule-item__icon alert-state-ok"
>
<i
className="icon-gf icon-gf-online"
/>
</span>
<div <div
className="alert-rule-item__body" className="page-container page-body"
> >
<div <div
className="alert-rule-item__header" className="page-action-bar"
> >
<div <div
className="alert-rule-item__name" className="gf-form gf-form--grow"
> >
<a <label
href="d/ufkcofof/my-goal?panelId=3&fullscreen=true&edit=true&tab=alert" className="gf-form--has-input-icon gf-form--grow"
> >
<Highlighter <input
highlightClassName="highlight-search-match" className="gf-form-input"
searchWords={ onChange={[Function]}
Array [ placeholder="Search alerts"
"", type="text"
] value=""
} />
textToHighlight="Panel Title alert" <i
> className="gf-form-input-icon fa fa-search"
<span> />
<span </label>
className=""
key="0"
>
Panel Title alert
</span>
</span>
</Highlighter>
</a>
</div> </div>
<div <div
className="alert-rule-item__text" className="gf-form"
> >
<span <label
className="alert-state-ok" className="gf-form-label"
> >
<Highlighter States
highlightClassName="highlight-search-match" </label>
searchWords={ <div
Array [ className="gf-form-select-wrapper width-13"
"", >
] <select
} className="gf-form-input"
textToHighlight="OK" onChange={[Function]}
value="all"
> >
<span> <option
<span key="all"
className="" value="all"
key="0" >
> All
OK </option>
</span> <option
</span> key="ok"
</Highlighter> value="ok"
</span> >
<span OK
className="alert-rule-item__time" </option>
> <option
for key="not_ok"
5 minutes value="not_ok"
</span> >
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>
<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> </div>
<section>
<ol
className="alert-rule-list"
>
<AlertRuleItem
key="1"
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"
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>
<div </div>
className="alert-rule-item__actions" `;
>
<button exports[`Render should render component 1`] = `
className="btn btn-small btn-inverse alert-list__btn width-2" <div>
onClick={[Function]} <PageHeader
title="Pausing an alert rule prevents it from executing" model={Object {}}
> />
<i <div
className="fa fa-pause" className="page-container page-body"
/> >
</button> <div
<a className="page-action-bar"
className="btn btn-small btn-inverse alert-list__btn width-2" >
href="d/ufkcofof/my-goal?panelId=3&fullscreen=true&edit=true&tab=alert" <div
title="Edit alert rule" className="gf-form gf-form--grow"
> >
<i <label
className="icon-gf icon-gf-settings" className="gf-form--has-input-icon gf-form--grow"
/> >
</a> <input
</div> className="gf-form-input"
</li> 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>
`; `;

View File

@ -1,6 +1,6 @@
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
import { AlertRule, StoreState } from 'app/types'; import { AlertRuleApi, StoreState } from 'app/types';
export enum ActionTypes { export enum ActionTypes {
LoadAlertRules = 'LOAD_ALERT_RULES', LoadAlertRules = 'LOAD_ALERT_RULES',
@ -9,7 +9,7 @@ export enum ActionTypes {
export interface LoadAlertRulesAction { export interface LoadAlertRulesAction {
type: ActionTypes.LoadAlertRules; type: ActionTypes.LoadAlertRules;
payload: AlertRule[]; payload: AlertRuleApi[];
} }
export interface SetSearchQueryAction { export interface SetSearchQueryAction {
@ -17,7 +17,7 @@ export interface SetSearchQueryAction {
payload: string; payload: string;
} }
export const loadAlertRules = (rules: AlertRule[]): LoadAlertRulesAction => ({ export const loadAlertRules = (rules: AlertRuleApi[]): LoadAlertRulesAction => ({
type: ActionTypes.LoadAlertRules, type: ActionTypes.LoadAlertRules,
payload: rules, payload: rules,
}); });
@ -31,7 +31,7 @@ export type Action = LoadAlertRulesAction | SetSearchQueryAction;
export const getAlertRulesAsync = (options: { state: string }) => async ( export const getAlertRulesAsync = (options: { state: string }) => async (
dispatch: Dispatch<Action> dispatch: Dispatch<Action>
): Promise<AlertRule[]> => { ): Promise<AlertRuleApi[]> => {
try { try {
const rules = await getBackendSrv().get('/api/alerts', options); const rules = await getBackendSrv().get('/api/alerts', options);
dispatch(loadAlertRules(rules)); dispatch(loadAlertRules(rules));

View 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);
});
});

View File

@ -1,40 +1,41 @@
import moment from 'moment'; import moment from 'moment';
import { AlertRulesState } from 'app/types'; import { AlertRuleApi, AlertRule, AlertRulesState } from 'app/types';
import { Action, ActionTypes } from './actions'; import { Action, ActionTypes } from './actions';
import alertDef from './alertDef'; import alertDef from './alertDef';
export const initialState: AlertRulesState = { items: [], searchQuery: '' }; export const initialState: AlertRulesState = { items: [], searchQuery: '' };
export function setStateFields(rule, state) { function convertToAlertRule(rule, state): AlertRule {
const stateModel = alertDef.getStateDisplayModel(state); const stateModel = alertDef.getStateDisplayModel(state);
rule.state = state;
rule.stateText = stateModel.text; rule.stateText = stateModel.text;
rule.stateIcon = stateModel.iconClass; rule.stateIcon = stateModel.iconClass;
rule.stateClass = stateModel.stateClass; rule.stateClass = stateModel.stateClass;
rule.stateAge = moment(rule.newStateDate) rule.stateAge = moment(rule.newStateDate)
.fromNow() .fromNow()
.replace(' ago', ''); .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 => { export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => {
switch (action.type) { switch (action.type) {
case ActionTypes.LoadAlertRules: { case ActionTypes.LoadAlertRules: {
const alertRules = action.payload; const alertRules: AlertRuleApi[] = action.payload;
for (const rule of alertRules) { const alertRulesViewModel: AlertRule[] = alertRules.map(rule => {
setStateFields(rule, rule.state); return convertToAlertRule(rule, rule.state);
});
if (rule.state !== 'paused') { return { items: alertRulesViewModel, searchQuery: state.searchQuery };
if (rule.executionError) {
rule.info = 'Execution Error: ' + rule.executionError;
}
if (rule.evalData && rule.evalData.noData) {
rule.info = 'Query returned no data';
}
}
}
return { items: alertRules, searchQuery: state.searchQuery };
} }
case ActionTypes.SetSearchQuery: case ActionTypes.SetSearchQuery:

View File

@ -22,6 +22,21 @@ export type UrlQueryMap = { [s: string]: UrlQueryValue };
// Alerting // 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 { export interface AlertRule {
id: number; id: number;
dashboardId: number; dashboardId: number;