From 64d4e6fcaab1187ec3e5c009589ecb9594a85d53 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 18 Mar 2022 14:33:32 +0100 Subject: [PATCH] Alerting: visual alert type picker (#46111) --- .../grafana-ui/src/components/Card/Card.tsx | 2 +- .../alerting/unified/RuleEditor.test.tsx | 83 ++++++++++++++----- .../components/rule-editor/AlertRuleForm.tsx | 1 + .../components/rule-editor/AlertTypeStep.tsx | 81 ++++++------------ .../rule-types/CortexOrLokiAlert.tsx | 32 +++++++ .../rule-types/CortexOrLokiRecordingRule.tsx | 28 +++++++ .../rule-types/DisabledTooltip.tsx | 20 +++++ .../rule-types/GrafanaManagedAlert.tsx | 25 ++++++ .../rule-editor/rule-types/RuleType.tsx | 52 ++++++++++++ .../rule-editor/rule-types/RuleTypePicker.tsx | 52 ++++++++++++ public/img/alerting/cortex_logo.svg | 1 + public/img/alerting/cortex_logo_recording.svg | 1 + 12 files changed, 301 insertions(+), 77 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rule-editor/rule-types/CortexOrLokiAlert.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/rule-types/CortexOrLokiRecordingRule.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/rule-types/DisabledTooltip.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/rule-types/GrafanaManagedAlert.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/rule-types/RuleType.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx create mode 100644 public/img/alerting/cortex_logo.svg create mode 100644 public/img/alerting/cortex_logo_recording.svg diff --git a/packages/grafana-ui/src/components/Card/Card.tsx b/packages/grafana-ui/src/components/Card/Card.tsx index 34524c1e7de..996339811c8 100644 --- a/packages/grafana-ui/src/components/Card/Card.tsx +++ b/packages/grafana-ui/src/components/Card/Card.tsx @@ -115,7 +115,7 @@ const Heading = ({ children, className, 'aria-label': ariaLabel }: ChildProps & ) : ( <>{children} )} - {isSelected !== undefined && } + {isSelected !== undefined && } ); }; diff --git a/public/app/features/alerting/unified/RuleEditor.test.tsx b/public/app/features/alerting/unified/RuleEditor.test.tsx index fa087d01708..b1fa89e3945 100644 --- a/public/app/features/alerting/unified/RuleEditor.test.tsx +++ b/public/app/features/alerting/unified/RuleEditor.test.tsx @@ -17,8 +17,8 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; import { DashboardSearchHit } from 'app/features/search/types'; import { getDefaultQueries } from './utils/rule-form'; import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor'; -import * as api from 'app/features/manage-dashboards/state/actions'; import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto'; +import { searchFolders } from '../../../../app/features/manage-dashboards/state/actions'; jest.mock('./components/rule-editor/ExpressionEditor', () => ({ // eslint-disable-next-line react/display-name @@ -29,6 +29,7 @@ jest.mock('./components/rule-editor/ExpressionEditor', () => ({ jest.mock('./api/ruler'); jest.mock('./utils/config'); +jest.mock('../../../../app/features/manage-dashboards/state/actions'); // there's no angular scope in test and things go terribly wrong when trying to render the query editor row. // lets just skip it @@ -39,7 +40,7 @@ jest.mock('app/features/query/components/QueryEditorRow', () => ({ const mocks = { getAllDataSources: jest.mocked(getAllDataSources), - + searchFolders: jest.mocked(searchFolders), api: { fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup), setRulerRuleGroup: jest.mocked(setRulerRuleGroup), @@ -80,6 +81,10 @@ const ui = { save: byRole('button', { name: 'Save' }), addAnnotation: byRole('button', { name: /Add info/ }), addLabel: byRole('button', { name: /Add label/ }), + // alert type buttons + grafanaManagedAlert: byRole('button', { name: /Grafana managed/ }), + lotexAlert: byRole('button', { name: /Cortex or Loki alert/ }), + lotexRecordingRule: byRole('button', { name: /Cortex or Loki recording rule/ }), }, }; @@ -123,10 +128,13 @@ describe('RuleEditor', () => { }, ], }); + mocks.searchFolders.mockResolvedValue([]); await renderRuleEditor(); + await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); + userEvent.type(await ui.inputs.name.find(), 'my great new rule'); - await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed alert/); + userEvent.click(await ui.buttons.lotexAlert.get()); const dataSourceSelect = ui.inputs.dataSource.get(); userEvent.click(byRole('combobox').get(dataSourceSelect)); await clickSelectOption(dataSourceSelect, 'Prom (default)'); @@ -165,7 +173,40 @@ describe('RuleEditor', () => { }); it('can create new grafana managed alert', async () => { - const searchFolderMock = jest.spyOn(api, 'searchFolders').mockResolvedValue([ + const dataSources = { + default: mockDataSource( + { + type: 'prometheus', + name: 'Prom', + isDefault: true, + }, + { alerting: true } + ), + }; + + setDataSourceSrv(new MockDataSourceSrv(dataSources)); + mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); + mocks.api.setRulerRuleGroup.mockResolvedValue(); + mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); + mocks.api.fetchRulerRulesGroup.mockResolvedValue({ + name: 'group2', + rules: [], + }); + mocks.api.fetchRulerRules.mockResolvedValue({ + namespace1: [ + { + name: 'group1', + rules: [], + }, + ], + namespace2: [ + { + name: 'group2', + rules: [], + }, + ], + }); + mocks.searchFolders.mockResolvedValue([ { title: 'Folder A', id: 1, @@ -176,24 +217,13 @@ describe('RuleEditor', () => { }, ] as DashboardSearchHit[]); - const dataSources = { - default: mockDataSource({ - type: 'prometheus', - name: 'Prom', - isDefault: true, - }), - }; - - setDataSourceSrv(new MockDataSourceSrv(dataSources)); - mocks.api.setRulerRuleGroup.mockResolvedValue(); - mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); - // fill out the form await renderRuleEditor(); + await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); + userEvent.type(await ui.inputs.name.find(), 'my great new rule'); - await clickSelectOption(ui.inputs.alertType.get(), /Classic Grafana alerts based on thresholds/); + const folderInput = await ui.inputs.folder.find(); - await waitFor(() => expect(searchFolderMock).toHaveBeenCalled()); await clickSelectOption(folderInput, 'Folder A'); userEvent.type(ui.inputs.annotationValue(0).get(), 'some summary'); @@ -264,12 +294,17 @@ describe('RuleEditor', () => { }, ], }); + mocks.searchFolders.mockResolvedValue([]); await renderRuleEditor(); + await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); + userEvent.type(await ui.inputs.name.find(), 'my great new recording rule'); - await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed recording rule/); + userEvent.click(await ui.buttons.lotexRecordingRule.get()); + const dataSourceSelect = ui.inputs.dataSource.get(); userEvent.click(byRole('combobox').get(dataSourceSelect)); + await clickSelectOption(dataSourceSelect, 'Prom (default)'); await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled()); await clickSelectOption(ui.inputs.namespace.get(), 'namespace2'); @@ -320,7 +355,6 @@ describe('RuleEditor', () => { uid: 'abcd', id: 1, }; - const searchFolderMock = jest.spyOn(api, 'searchFolders').mockResolvedValue([folder] as DashboardSearchHit[]); const getFolderByUid = jest.fn().mockResolvedValue({ ...folder, canSave: true, @@ -338,6 +372,7 @@ describe('RuleEditor', () => { } as any as BackendSrv; setBackendSrv(backendSrv); setDataSourceSrv(new MockDataSourceSrv(dataSources)); + mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.api.setRulerRuleGroup.mockResolvedValue(); mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]); mocks.api.fetchRulerRules.mockResolvedValue({ @@ -365,9 +400,10 @@ describe('RuleEditor', () => { }, ], }); + mocks.searchFolders.mockResolvedValue([folder] as DashboardSearchHit[]); await renderRuleEditor(uid); - await waitFor(() => expect(searchFolderMock).toHaveBeenCalled()); + await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); // check that it's filled in const nameInput = await ui.inputs.name.find(); @@ -484,11 +520,14 @@ describe('RuleEditor', () => { setDataSourceSrv(new MockDataSourceSrv(dataSources)); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); + mocks.searchFolders.mockResolvedValue([]); // render rule editor, select cortex/loki managed alerts await renderRuleEditor(); + await waitFor(() => expect(mocks.searchFolders).toHaveBeenCalled()); + await ui.inputs.name.find(); - await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed alert/); + userEvent.click(await ui.buttons.lotexAlert.get()); // wait for ui theck each datasource if it supports rule editing await waitFor(() => expect(mocks.api.fetchRulerRulesGroup).toHaveBeenCalledTimes(4)); diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx index 9d18b639290..8de196cbd47 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx @@ -45,6 +45,7 @@ export const AlertRuleForm: FC = ({ existing }) => { ...getDefaultFormValues(), queries: getDefaultQueries(), ...(queryParams['defaults'] ? JSON.parse(queryParams['defaults'] as string) : {}), + type: RuleFormType.grafana, }; }, [existing, queryParams]); diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx index 4a96553d037..aa2179f2265 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx @@ -1,15 +1,15 @@ -import React, { FC, useMemo } from 'react'; -import { DataSourceInstanceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Field, Input, InputControl, Select, useStyles2 } from '@grafana/ui'; +import React, { FC } from 'react'; +import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data'; +import { Field, Input, InputControl, useStyles2 } from '@grafana/ui'; import { css } from '@emotion/css'; import { RuleEditorSection } from './RuleEditorSection'; import { useFormContext } from 'react-hook-form'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { Folder, RuleFolderPicker } from './RuleFolderPicker'; import { GroupAndNamespaceFields } from './GroupAndNamespaceFields'; -import { contextSrv } from 'app/core/services/context_srv'; import { CloudRulesSourcePicker } from './CloudRulesSourcePicker'; import { checkForPathSeparator } from './util'; +import { RuleTypePicker } from './rule-types/RuleTypePicker'; interface Props { editingExistingRule: boolean; @@ -30,38 +30,36 @@ export const AlertTypeStep: FC = ({ editingExistingRule }) => { watch, formState: { errors }, setValue, + getValues, } = useFormContext(); const ruleFormType = watch('type'); const dataSourceName = watch('dataSourceName'); - const alertTypeOptions = useMemo((): SelectableValue[] => { - const result = [ - { - label: 'Grafana managed alert', - value: RuleFormType.grafana, - description: 'Classic Grafana alerts based on thresholds.', - }, - ]; - - if (contextSrv.isEditor) { - result.push({ - label: 'Cortex/Loki managed alert', - value: RuleFormType.cloudAlerting, - description: 'Alert based on a system or application behavior. Based on Prometheus.', - }); - result.push({ - label: 'Cortex/Loki managed recording rule', - value: RuleFormType.cloudRecording, - description: 'Recording rule to pre-compute frequently needed or expensive calculations. Based on Prometheus.', - }); - } - - return result; - }, []); - return ( + + ( + + )} + name="type" + control={control} + rules={{ + required: { value: true, message: 'Please select alert type' }, + }} + /> + + = ({ editingExistingRule }) => { />
- - ( -