From e8e84f9c236fb3b5f210659270784f77938c55a8 Mon Sep 17 00:00:00 2001 From: Nathan Rodman Date: Wed, 3 Nov 2021 09:57:59 -0700 Subject: [PATCH] Alerting: UI for contact point testing with custom annotations and labels (#40491) * Support custom annotations and labels when testing contact points * Add modal for testing contact point * add option for custom notification type * use annotation and labels fields from rule editor * update receivers test for new contact point testing method * rename modal and remove reserved keys for annotations * add docs for testing contact points Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: George Robinson Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> --- .../unified-alerting/contact-points.md | 12 ++ .../alerting/unified/Receivers.test.tsx | 50 +++++--- .../alerting/unified/api/alertmanager.ts | 8 +- .../receivers/form/GrafanaReceiverForm.tsx | 54 +++++--- .../receivers/form/TestContactPointModal.tsx | 117 ++++++++++++++++++ .../alerting/unified/state/actions.ts | 6 +- .../plugins/datasource/alertmanager/types.ts | 3 + 7 files changed, 215 insertions(+), 35 deletions(-) create mode 100644 public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx diff --git a/docs/sources/alerting/unified-alerting/contact-points.md b/docs/sources/alerting/unified-alerting/contact-points.md index 15c727230e4..3c6e6109dc7 100644 --- a/docs/sources/alerting/unified-alerting/contact-points.md +++ b/docs/sources/alerting/unified-alerting/contact-points.md @@ -30,6 +30,18 @@ You can configure Grafana managed contact points as well as contact points for a 1. Find the contact point to edit, then click **Edit** (pen icon). 1. Make any changes and click **Save contact point**. +## Test a contact point + +For Grafana managed contact points, you can send a test notification which helps verify a contact point is configured correctly. + +To send a test notification: + +1. In the Grafana side bar, hover your cursor over the **Alerting** (bell) icon and then click **Contact** points. +1. Find the contact point to test, then click **Edit** (pen icon). You can also create a new contact point if needed. +1. Click **Test** (paper airplane icon) to open the contact point testing modal. +1. Choose whether to send a predefined test notification or choose custom to add your own custom annotations and labels to include in the notification. +1. Click **Send test notification** to fire the alert. + ## Delete a contact point 1. In the Alerting page, click **Contact points** to open the page listing existing contact points. diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index f128b137486..69c09a573ee 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -78,6 +78,13 @@ const ui = { saveContactButton: byRole('button', { name: /save contact point/i }), newContactPointTypeButton: byRole('button', { name: /new contact point type/i }), testContactPointButton: byRole('button', { name: /Test/ }), + testContactPointModal: byRole('heading', { name: /test contact point/i }), + customContactPointOption: byRole('radio', { name: /custom/i }), + contactPointAnnotationSelect: (idx: number) => byTestId(`annotation-key-${idx}`), + contactPointAnnotationValue: (idx: number) => byTestId(`annotation-value-${idx}`), + contactPointLabelKey: (idx: number) => byTestId(`label-key-${idx}`), + contactPointLabelValue: (idx: number) => byTestId(`label-value-${idx}`), + testContactPoint: byRole('button', { name: /send test notification/i }), cancelButton: byTestId('cancel-button'), receiversTable: byTestId('receivers-table'), @@ -183,22 +190,37 @@ describe('Receivers', () => { // try to test the contact point userEvent.click(ui.testContactPointButton.get()); + await waitFor(() => expect(ui.testContactPointModal.get()).toBeInTheDocument()); + userEvent.click(ui.customContactPointOption.get()); + await waitFor(() => expect(ui.contactPointAnnotationSelect(0).get()).toBeInTheDocument()); + + // enter custom annotations and labels + await clickSelectOption(ui.contactPointAnnotationSelect(0).get(), 'Description'); + await userEvent.type(ui.contactPointAnnotationValue(0).get(), 'Test contact point'); + await userEvent.type(ui.contactPointLabelKey(0).get(), 'foo'); + await userEvent.type(ui.contactPointLabelValue(0).get(), 'bar'); + userEvent.click(ui.testContactPoint.get()); + await waitFor(() => expect(mocks.api.testReceivers).toHaveBeenCalled()); - expect(mocks.api.testReceivers).toHaveBeenCalledWith('grafana', [ - { - grafana_managed_receiver_configs: [ - { - disableResolveMessage: false, - name: 'test', - secureSettings: {}, - settings: { addresses: 'tester@grafana.com', singleEmail: false }, - type: 'email', - }, - ], - name: 'test', - }, - ]); + expect(mocks.api.testReceivers).toHaveBeenCalledWith( + 'grafana', + [ + { + grafana_managed_receiver_configs: [ + { + disableResolveMessage: false, + name: 'test', + secureSettings: {}, + settings: { addresses: 'tester@grafana.com', singleEmail: false }, + type: 'email', + }, + ], + name: 'test', + }, + ], + { annotations: { description: 'Test contact point' }, labels: { foo: 'bar' } } + ); }); it('Grafana receiver can be created', async () => { diff --git a/public/app/features/alerting/unified/api/alertmanager.ts b/public/app/features/alerting/unified/api/alertmanager.ts index 58015cd5e6f..291de5fd49e 100644 --- a/public/app/features/alerting/unified/api/alertmanager.ts +++ b/public/app/features/alerting/unified/api/alertmanager.ts @@ -13,6 +13,7 @@ import { Receiver, TestReceiversPayload, TestReceiversResult, + TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; @@ -160,9 +161,14 @@ export async function fetchStatus(alertManagerSourceName: string): Promise { +export async function testReceivers( + alertManagerSourceName: string, + receivers: Receiver[], + alert?: TestReceiversAlert +): Promise { const data: TestReceiversPayload = { receivers, + alert, }; const result = await lastValueFrom( getBackendSrv().fetch({ diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx index dc5c3321e0a..36528cebe9d 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx @@ -3,8 +3,9 @@ import { AlertManagerCortexConfig, GrafanaManagedReceiverConfig, Receiver, + TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; -import React, { FC, useEffect, useMemo } from 'react'; +import React, { FC, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; import { @@ -22,6 +23,7 @@ import { } from '../../../utils/receiver-form'; import { GrafanaCommonChannelSettings } from './GrafanaCommonChannelSettings'; import { ReceiverForm } from './ReceiverForm'; +import { TestContactPointModal } from './TestContactPointModal'; interface Props { alertManagerSourceName: string; @@ -40,6 +42,7 @@ const defaultChannelValues: GrafanaChannelValues = Object.freeze({ export const GrafanaReceiverForm: FC = ({ existing, alertManagerSourceName, config }) => { const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers); + const [testChannelValues, setTestChannelValues] = useState(); const dispatch = useDispatch(); @@ -74,10 +77,15 @@ export const GrafanaReceiverForm: FC = ({ existing, alertManagerSourceNam }; const onTestChannel = (values: GrafanaChannelValues) => { - const existing: GrafanaManagedReceiverConfig | undefined = id2original[values.__id]; - const chan = formChannelValuesToGrafanaChannelConfig(values, defaultChannelValues, 'test', existing); - dispatch( - testReceiversAction({ + setTestChannelValues(values); + }; + + const testNotification = (alert?: TestReceiversAlert) => { + if (testChannelValues) { + const existing: GrafanaManagedReceiverConfig | undefined = id2original[testChannelValues.__id]; + const chan = formChannelValuesToGrafanaChannelConfig(testChannelValues, defaultChannelValues, 'test', existing); + + const payload = { alertManagerSourceName, receivers: [ { @@ -85,8 +93,11 @@ export const GrafanaReceiverForm: FC = ({ existing, alertManagerSourceNam grafana_managed_receiver_configs: [chan], }, ], - }) - ); + alert, + }; + + dispatch(testReceiversAction(payload)); + } }; const takenReceiverNames = useMemo( @@ -96,17 +107,24 @@ export const GrafanaReceiverForm: FC = ({ existing, alertManagerSourceNam if (grafanaNotifiers.result) { return ( - - config={config} - onSubmit={onSubmit} - initialValues={existingValue} - onTestChannel={onTestChannel} - notifiers={grafanaNotifiers.result} - alertManagerSourceName={alertManagerSourceName} - defaultItem={defaultChannelValues} - takenReceiverNames={takenReceiverNames} - commonSettingsComponent={GrafanaCommonChannelSettings} - /> + <> + + config={config} + onSubmit={onSubmit} + initialValues={existingValue} + onTestChannel={onTestChannel} + notifiers={grafanaNotifiers.result} + alertManagerSourceName={alertManagerSourceName} + defaultItem={defaultChannelValues} + takenReceiverNames={takenReceiverNames} + commonSettingsComponent={GrafanaCommonChannelSettings} + /> + setTestChannelValues(undefined)} + isOpen={!!testChannelValues} + onTest={(alert) => testNotification(alert)} + /> + ); } else { return ; diff --git a/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx b/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx new file mode 100644 index 00000000000..1c580b64902 --- /dev/null +++ b/public/app/features/alerting/unified/components/receivers/form/TestContactPointModal.tsx @@ -0,0 +1,117 @@ +import React, { useState } from 'react'; +import { Modal, Button, Label, useStyles2, RadioButtonGroup } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useForm, FormProvider } from 'react-hook-form'; +import { TestReceiversAlert } from 'app/plugins/datasource/alertmanager/types'; +import { css } from '@emotion/css'; +import AnnotationsField from '../../rule-editor/AnnotationsField'; +import LabelsField from '../../rule-editor/LabelsField'; +import { Annotations, Labels } from 'app/types/unified-alerting-dto'; + +interface Props { + isOpen: boolean; + onDismiss: () => void; + onTest: (alert?: TestReceiversAlert) => void; +} + +type AnnoField = { + key: string; + value: string; +}; + +interface FormFields { + annotations: AnnoField[]; + labels: AnnoField[]; +} + +enum NotificationType { + predefined = 'Predefined', + custom = 'Custom', +} + +const notificationOptions = Object.values(NotificationType).map((value) => ({ label: value, value: value })); + +const defaultValues: FormFields = { + annotations: [{ key: '', value: '' }], + labels: [{ key: '', value: '' }], +}; + +export const TestContactPointModal = ({ isOpen, onDismiss, onTest }: Props) => { + const [notificationType, setNotificationType] = useState(NotificationType.predefined); + const styles = useStyles2(getStyles); + const formMethods = useForm({ defaultValues, mode: 'onBlur' }); + + const onSubmit = (data: FormFields) => { + if (notificationType === NotificationType.custom) { + const alert = { + annotations: data.annotations + .filter(({ key, value }) => !!key && !!value) + .reduce((acc, { key, value }) => { + return { ...acc, [key]: value }; + }, {} as Annotations), + labels: data.labels + .filter(({ key, value }) => !!key && !!value) + .reduce((acc, { key, value }) => { + return { ...acc, [key]: value }; + }, {} as Labels), + }; + onTest(alert); + } else { + onTest(); + } + }; + + return ( + +
+ + setNotificationType(value)} + /> +
+ + +
+ {notificationType === NotificationType.predefined && ( +
+ You will send a test notification that uses a predefined alert. If you have defined a custom template or + message, for better results switch to custom notification message, from above. +
+ )} + {notificationType === NotificationType.custom && ( + <> +
+ You will send a test notification that uses the annotations defined below. This is a good option if you + use custom templates and messages. +
+
+ +
+
+ +
+ + )} + + + + +
+
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + flexRow: css` + display: flex; + flex-direction: row; + align-items: flex-start; + margin-bottom: ${theme.spacing(1)}; + `, + section: css` + margin-bottom: ${theme.spacing(2)}; + `, +}); diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 24ea7b2a443..3e16ad34c7c 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -7,6 +7,7 @@ import { Receiver, Silence, SilenceCreatePayload, + TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; import { FolderDTO, NotifierDTO, ThunkResult } from 'app/types'; import { RuleIdentifier, RuleNamespace, RuleWithLocation } from 'app/types/unified-alerting'; @@ -633,12 +634,13 @@ export const deleteAlertManagerConfigAction = createAsyncThunk( interface TestReceiversOptions { alertManagerSourceName: string; receivers: Receiver[]; + alert?: TestReceiversAlert; } export const testReceiversAction = createAsyncThunk( 'unifiedalerting/testReceivers', - ({ alertManagerSourceName, receivers }: TestReceiversOptions): Promise => { - return withAppEvents(withSerializedError(testReceivers(alertManagerSourceName, receivers)), { + ({ alertManagerSourceName, receivers, alert }: TestReceiversOptions): Promise => { + return withAppEvents(withSerializedError(testReceivers(alertManagerSourceName, receivers, alert)), { errorMessage: 'Failed to send test alert.', successMessage: 'Test alert sent.', }); diff --git a/public/app/plugins/datasource/alertmanager/types.ts b/public/app/plugins/datasource/alertmanager/types.ts index 408c51f6dd9..4a44690ebd1 100644 --- a/public/app/plugins/datasource/alertmanager/types.ts +++ b/public/app/plugins/datasource/alertmanager/types.ts @@ -234,8 +234,11 @@ export interface AlertmanagerStatus { }; } +export type TestReceiversAlert = Pick; + export interface TestReceiversPayload { receivers?: Receiver[]; + alert?: TestReceiversAlert; } interface TestReceiversResultGrafanaReceiverConfig {