Peter Holmberg 7 лет назад
Родитель
Сommit
22510be450

+ 2 - 1
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(<AlertRuleItem {...props} />);

+ 1 - 1
public/app/features/alerting/AlertRuleItem.tsx

@@ -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) {

+ 141 - 51
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';
-
-describe('AlertRuleList', () => {
-  let page, store;
-
-  beforeAll(() => {
-    backendSrv.get.mockReturnValue(
-      Promise.resolve([
+import { shallow } from 'enzyme';
+import AlertRuleList, { Props } from './AlertRuleList';
+import { AlertRule, NavModel } from '../../types';
+import appEvents from '../../core/app_events';
+
+jest.mock('react-redux', () => ({
+  connect: () => params => params,
+}));
+
+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} />);
+      ],
+    });
+
+    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();
+    });
   });
 
-  it('should call api to get rules', () => {
-    expect(backendSrv.get.mock.calls[0][0]).toEqual('/api/alerts');
+  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',
+      });
 
-  it('should render 1 rule', () => {
-    page.update();
-    const ruleNode = page.find('.alert-rule-item');
-    expect(toJson(ruleNode)).toMatchSnapshot();
+      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' } });
+    });
   });
 
-  it('toggle state should change pause rule if not paused', async () => {
-    backendSrv.post.mockReturnValue(
-      Promise.resolve({
-        state: 'paused',
-      })
-    );
+  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: {},
+      });
+    });
+  });
 
-    page.find('.fa-pause').simulate('click');
+  describe('Search query change', () => {
+    it('should set search query', () => {
+      const { instance } = setup();
+      const mockEvent = { target: { value: 'dashboard' } };
 
-    // wait for api call to resolve
-    await Promise.resolve();
-    page.update();
+      instance.onSearchQueryChange(mockEvent);
 
-    expect(store.alertList.rules[0].state).toBe('paused');
-    expect(page.find('.fa-play')).toHaveLength(1);
+      expect(instance.props.setSearchQuery).toHaveBeenCalledWith('dashboard');
+    });
   });
 });

+ 12 - 19
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<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} />)}

+ 230 - 79
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`] = `
-<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"
-          >
-            <span>
-              <span
-                className=""
-                key="0"
-              >
-                OK
-              </span>
-            </span>
-          </Highlighter>
-        </span>
-        <span
-          className="alert-rule-item__time"
+          States
+        </label>
+        <div
+          className="gf-form-select-wrapper width-13"
         >
-           for 
-          5 minutes
-        </span>
+          <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"
+          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>
+`;
+
+exports[`Render should render component 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
   <div
-    className="alert-rule-item__actions"
+    className="page-container page-body"
   >
-    <button
-      className="btn btn-small btn-inverse alert-list__btn width-2"
-      onClick={[Function]}
-      title="Pausing an alert rule prevents it from executing"
+    <div
+      className="page-action-bar"
     >
-      <i
-        className="fa fa-pause"
+      <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"
       />
-    </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
+        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"
       />
-    </a>
+    </section>
   </div>
-</li>
+</div>
 `;

+ 4 - 4
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<Action>
-): Promise<AlertRule[]> => {
+): Promise<AlertRuleApi[]> => {
   try {
     const rules = await getBackendSrv().get('/api/alerts', options);
     dispatch(loadAlertRules(rules));

+ 91 - 0
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);
+  });
+});

+ 20 - 19
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;
-
-      for (const rule of alertRules) {
-        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';
-          }
-        }
-      }
-
-      return { items: alertRules, searchQuery: state.searchQuery };
+      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:

+ 15 - 0
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;