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

View File

@ -15,7 +15,7 @@ class AlertRuleItem extends PureComponent<Props, any> {
togglePaused = () => {
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) {

View File

@ -1,69 +1,159 @@
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';
import { shallow } from 'enzyme';
import AlertRuleList, { Props } from './AlertRuleList';
import { AlertRule, NavModel } from '../../types';
import appEvents from '../../core/app_events';
describe('AlertRuleList', () => {
let page, store;
jest.mock('react-redux', () => ({
connect: () => params => params,
}));
beforeAll(() => {
backendSrv.get.mockReturnValue(
Promise.resolve([
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(),
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,
dashboardId: 58,
id: 1,
dashboardId: 7,
dashboardUid: 'ggHbN42mk',
dashboardSlug: 'alerting-with-testdata',
panelId: 3,
name: 'Panel Title alert',
name: 'TestData - Always OK',
state: 'ok',
newStateDate: moment()
.subtract(5, 'minutes')
.format(),
newStateDate: '2018-09-04T10:01:01+02:00',
evalDate: '0001-01-01T00:00:00Z',
evalData: {},
executionError: '',
url: 'd/ufkcofof/my-goal',
canEdit: true,
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',
},
],
});
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);
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' });
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');
});
});
});

View File

@ -10,7 +10,7 @@ import { NavModel, StoreState, AlertRule } from 'app/types';
import { getAlertRulesAsync, setSearchQuery } from './state/actions';
import { getAlertRuleItems, getSearchQuery } from './state/selectors';
interface Props {
export interface Props {
navModel: NavModel;
alertRules: AlertRule[];
updateLocation: typeof updateLocation;
@ -20,11 +20,7 @@ interface Props {
search: string;
}
interface State {
search: string;
}
export class AlertRuleList extends PureComponent<Props, State> {
class AlertRuleList extends PureComponent<Props, any> {
stateFilters = [
{ text: 'All', value: 'all' },
{ text: 'OK', value: 'ok' },
@ -44,11 +40,9 @@ export class AlertRuleList extends PureComponent<Props, State> {
}
}
onStateFilterChanged = evt => {
this.props.updateLocation({
query: { state: evt.target.value },
});
};
async fetchRules() {
await this.props.getAlertRulesAsync({ state: this.getStateFilter() });
}
getStateFilter(): string {
const { stateFilter } = this.props;
@ -58,9 +52,11 @@ export class AlertRuleList extends PureComponent<Props, State> {
return 'all';
}
async fetchRules() {
await this.props.getAlertRulesAsync({ state: this.getStateFilter() });
}
onStateFilterChanged = event => {
this.props.updateLocation({
query: { state: event.target.value },
});
};
onOpenHowTo = () => {
appEvents.emit('show-modal', {
@ -75,13 +71,13 @@ export class AlertRuleList extends PureComponent<Props, State> {
this.props.setSearchQuery(value);
};
alertStateFilterOption({ text, value }) {
alertStateFilterOption = ({ text, value }) => {
return (
<option key={value} value={value}>
{text}
</option>
);
}
};
render() {
const { navModel, alertRules, search } = this.props;
@ -112,14 +108,11 @@ export class AlertRuleList extends PureComponent<Props, State> {
</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} />)}

View File

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

View File

@ -1,6 +1,6 @@
import { Dispatch } from 'redux';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { AlertRule, StoreState } from 'app/types';
import { AlertRuleApi, StoreState } from 'app/types';
export enum ActionTypes {
LoadAlertRules = 'LOAD_ALERT_RULES',
@ -9,7 +9,7 @@ export enum ActionTypes {
export interface LoadAlertRulesAction {
type: ActionTypes.LoadAlertRules;
payload: AlertRule[];
payload: AlertRuleApi[];
}
export interface SetSearchQueryAction {
@ -17,7 +17,7 @@ export interface SetSearchQueryAction {
payload: string;
}
export const loadAlertRules = (rules: AlertRule[]): LoadAlertRulesAction => ({
export const loadAlertRules = (rules: AlertRuleApi[]): LoadAlertRulesAction => ({
type: ActionTypes.LoadAlertRules,
payload: rules,
});
@ -31,7 +31,7 @@ export type Action = LoadAlertRulesAction | SetSearchQueryAction;
export const getAlertRulesAsync = (options: { state: string }) => async (
dispatch: Dispatch<Action>
): Promise<AlertRule[]> => {
): Promise<AlertRuleApi[]> => {
try {
const rules = await getBackendSrv().get('/api/alerts', options);
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 { AlertRulesState } from 'app/types';
import { AlertRuleApi, AlertRule, AlertRulesState } from 'app/types';
import { Action, ActionTypes } from './actions';
import alertDef from './alertDef';
export const initialState: AlertRulesState = { items: [], searchQuery: '' };
export function setStateFields(rule, state) {
function convertToAlertRule(rule, state): AlertRule {
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', '');
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 = action.payload;
const alertRules: AlertRuleApi[] = action.payload;
for (const rule of alertRules) {
setStateFields(rule, rule.state);
const alertRulesViewModel: AlertRule[] = alertRules.map(rule => {
return convertToAlertRule(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';
}
}
}
return { items: alertRules, searchQuery: state.searchQuery };
return { items: alertRulesViewModel, searchQuery: state.searchQuery };
}
case ActionTypes.SetSearchQuery:

View File

@ -22,6 +22,21 @@ 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;