diff --git a/public/app/features/alerting/AlertRuleItem.test.tsx b/public/app/features/alerting/AlertRuleItem.test.tsx index 397c5c5ac0c..1b356fa5687 100644 --- a/public/app/features/alerting/AlertRuleItem.test.tsx +++ b/public/app/features/alerting/AlertRuleItem.test.tsx @@ -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(); diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index 95c6966ab88..0e6b1c5fb90 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -15,7 +15,7 @@ class AlertRuleItem extends PureComponent { 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) { diff --git a/public/app/features/alerting/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx index f88ff4522d4..9bcdcd41a3b 100644 --- a/public/app/features/alerting/AlertRuleList.test.tsx +++ b/public/app/features/alerting/AlertRuleList.test.tsx @@ -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(); + + 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(); - }); - - 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'); + }); }); }); diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 6023a1bb142..e8458e72f11 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -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 { +class AlertRuleList extends PureComponent { stateFilters = [ { text: 'All', value: 'all' }, { text: 'OK', value: 'ok' }, @@ -44,11 +40,9 @@ export class AlertRuleList extends PureComponent { } } - 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 { 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 { this.props.setSearchQuery(value); }; - alertStateFilterOption({ text, value }) { + alertStateFilterOption = ({ text, value }) => { return ( ); - } + }; render() { const { navModel, alertRules, search } = this.props; @@ -112,14 +108,11 @@ export class AlertRuleList extends PureComponent { - -
    {alertRules.map(rule => )} diff --git a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap index f408f6409be..99869ba6126 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap @@ -1,103 +1,254 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AlertRuleList should render 1 rule 1`] = ` -
  1. - - - +exports[`Render should render alert rules 1`] = ` +
    +
    - - +
    + +
    + +
    +
      + + +
    +
    -
    - - - - -
    -
  2. + +`; + +exports[`Render should render component 1`] = ` +
    + +
    +
    +
    + +
    +
    + +
    + +
    +
    + +
    +
      +
    +
    +
    `; diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index 87afbfff665..2dff257685f 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -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 -): Promise => { +): Promise => { try { const rules = await getBackendSrv().get('/api/alerts', options); dispatch(loadAlertRules(rules)); diff --git a/public/app/features/alerting/state/reducers.test.ts b/public/app/features/alerting/state/reducers.test.ts new file mode 100644 index 00000000000..96ca7bacf6c --- /dev/null +++ b/public/app/features/alerting/state/reducers.test.ts @@ -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); + }); +}); diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts index a18d112dd94..73feb3cb260 100644 --- a/public/app/features/alerting/state/reducers.ts +++ b/public/app/features/alerting/state/reducers.ts @@ -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: diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 1f17962a70b..debfcf58ac8 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -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;