From 2acf153a263ce876ac0905f3a08b6190034b82be Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 22 Nov 2023 10:15:33 +0100 Subject: [PATCH] Alerting: Simplify routing in alert form - part1 (#78040) * Add routing option tabs * Use alertingSimplifiedRouting feature toggle * Move simplified routing tab to a separate component:SimplifiedRouting * Populate contact point selector with the right values * Show alert manager icons * Fix descriptions * Remove clear button on ContactPointSelector and save updated reducer state in the form * Load contact points and manual option from rule data in RuleFormValues * make contact point selector not clearable * Refactor * Add link to contact points view * Move ContactPointSelector to a separate file * Refactor: move hoook useReceiversMetadataMapByName to a separate file * Update Need more info texts * Address some PR review comments * Use useContactPointsWithStatus hook and wrap each ContacPointSelector with AlertmanagerProvider * use getAlertManagerDataSourcesByPermission instead of useGetAlertManagersMetadata in NotificationPreview * Update enum * Remove css style * remove console * update contact point selector * file cleanup * adds summary as description * Update text in manual tab * Fix preview routing not checking if alert manager can handle grafana alerts * Fix typo * remove unused location form field * fix prettier * fix test * Remove unused location form field from AlertRuleNameInput * Only use internal AlertManager for now --------- Co-authored-by: Gilles De Mey --- .betterer.results | 10 - .../alerting/unified/api/buildInfo.test.ts | 4 +- .../contact-points/ContactPoints.v2.tsx | 47 ++- .../rule-editor/AlertRuleNameInput.tsx | 2 +- .../components/rule-editor/LabelsField.tsx | 126 ++++---- .../rule-editor/NotificationsStep.tsx | 275 ++++++++++++++---- .../RecordingRulesNameSpaceAndGroupStep.tsx | 2 +- .../ContactPointSelector.tsx | 65 +++++ .../simplifiedRouting/SimplifiedRouting.tsx | 153 ++++++++++ .../NotificationPreview.test.tsx | 45 +-- .../NotificationPreview.tsx | 13 +- .../NotificationPreviewByAlertManager.tsx | 6 +- ...useGetAlertManagersSourceNamesAndImage.tsx | 28 -- .../CloudDataSourceSelector.tsx | 4 +- .../state/AlertmanagerContext.test.tsx | 29 +- .../unified/state/AlertmanagerContext.tsx | 7 +- .../alerting/unified/types/rule-form.ts | 7 + .../alerting/unified/utils/datasource.ts | 61 +++- .../alerting/unified/utils/rule-form.ts | 4 + 19 files changed, 642 insertions(+), 246 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/ContactPointSelector.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRouting.tsx delete mode 100644 public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useGetAlertManagersSourceNamesAndImage.tsx diff --git a/.betterer.results b/.betterer.results index f2a8a36ed1c..0aa59dc2050 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2266,16 +2266,6 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] ], - "public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"] - ], "public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] diff --git a/public/app/features/alerting/unified/api/buildInfo.test.ts b/public/app/features/alerting/unified/api/buildInfo.test.ts index 5bc0e7f60a2..53f9319364a 100644 --- a/public/app/features/alerting/unified/api/buildInfo.test.ts +++ b/public/app/features/alerting/unified/api/buildInfo.test.ts @@ -10,7 +10,9 @@ const fetch = jest.fn(); jest.mock('./prometheus'); jest.mock('./ruler'); -jest.mock('app/core/services/context_srv', () => {}); +jest.mock('app/core/services/context_srv', () => ({ + contextSrv: jest.fn(), +})); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ fetch }), diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx index fc92842834d..c6e8cc2fe97 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx @@ -325,7 +325,7 @@ export const ContactPoint = ({ })} ) : ( -
+
)} @@ -493,35 +493,32 @@ type ContactPointReceiverSummaryProps = { * This summary is used when we're dealing with non-Grafana managed alertmanager since they * don't have any metadata worth showing other than a summary of what types are configured for the contact point */ -const ContactPointReceiverSummary = ({ receivers }: ContactPointReceiverSummaryProps) => { - const styles = useStyles2(getStyles); +export const ContactPointReceiverSummary = ({ receivers }: ContactPointReceiverSummaryProps) => { const countByType = groupBy(receivers, (receiver) => receiver.type); return ( -
- - - {Object.entries(countByType).map(([type, receivers], index) => { - const iconName = INTEGRATION_ICONS[type]; - const receiverName = receiverTypeNames[type] ?? upperFirst(type); - const isLastItem = size(countByType) - 1 === index; + + + {Object.entries(countByType).map(([type, receivers], index) => { + const iconName = INTEGRATION_ICONS[type]; + const receiverName = receiverTypeNames[type] ?? upperFirst(type); + const isLastItem = size(countByType) - 1 === index; - return ( - - - {iconName && } - - {receiverName} - {receivers.length > 1 && <> ({receivers.length})} - - - {!isLastItem && '⋅'} - - ); - })} - + return ( + + + {iconName && } + + {receiverName} + {receivers.length > 1 && <> ({receivers.length})} + + + {!isLastItem && '⋅'} + + ); + })} -
+ ); }; diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx index 5849565febb..f96d308e08e 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx @@ -18,7 +18,7 @@ export const AlertRuleNameInput = () => { register, watch, formState: { errors }, - } = useFormContext(); + } = useFormContext(); const ruleFormType = watch('type'); const entityName = ruleFormType === RuleFormType.cloudRecording ? 'recording rule' : 'alert rule'; diff --git a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx index fdeec0525ab..49e01b94660 100644 --- a/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx @@ -3,19 +3,7 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FieldArrayMethodProps, useFieldArray, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { - Button, - Field, - InlineLabel, - Label, - useStyles2, - Text, - Tooltip, - Icon, - Input, - LoadingPlaceholder, - Stack, -} from '@grafana/ui'; +import { Button, Field, InlineLabel, Input, LoadingPlaceholder, Stack, Text, useStyles2 } from '@grafana/ui'; import { useDispatch } from 'app/types'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; @@ -23,6 +11,8 @@ import { fetchRulerRulesIfNotFetchedYet } from '../../state/actions'; import { RuleFormValues } from '../../types/rule-form'; import AlertLabelDropdown from '../AlertLabelDropdown'; +import { NeedHelpInfo } from './NeedHelpInfo'; + interface Props { className?: string; dataSourceName?: string | null; @@ -271,23 +261,20 @@ const LabelsField: FC = ({ dataSourceName }) => { return (
-
- } - > - - + - + +
{dataSourceName ? : }
); @@ -295,47 +282,48 @@ const LabelsField: FC = ({ dataSourceName }) => { const getStyles = (theme: GrafanaTheme2) => { return { - icon: css` - margin-right: ${theme.spacing(0.5)}; - `, - flexColumn: css` - display: flex; - flex-direction: column; - `, - flexRow: css` - display: flex; - flex-direction: row; - justify-content: flex-start; - - & + button { - margin-left: ${theme.spacing(0.5)}; - } - `, - deleteLabelButton: css` - margin-left: ${theme.spacing(0.5)}; - align-self: flex-start; - `, - addLabelButton: css` - flex-grow: 0; - align-self: flex-start; - `, - centerAlignRow: css` - align-items: baseline; - `, - equalSign: css` - align-self: flex-start; - width: 28px; - justify-content: center; - margin-left: ${theme.spacing(0.5)}; - `, - labelInput: css` - width: 175px; - margin-bottom: -${theme.spacing(1)}; - - & + & { - margin-left: ${theme.spacing(1)}; - } - `, + icon: css({ + marginRight: theme.spacing(0.5), + }), + flexColumn: css({ + display: 'flex', + flexDirection: 'column', + }), + flexRow: css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + '& + button': { + marginLeft: theme.spacing(0.5), + }, + }), + deleteLabelButton: css({ + marginLeft: theme.spacing(0.5), + alignSelf: 'flex-start', + }), + addLabelButton: css({ + flexGrow: 0, + alignSelf: 'flex-start', + }), + centerAlignRow: css({ + alignItems: 'baseline', + }), + equalSign: css({ + alignSelf: 'flex-start', + width: '28px', + justifyContent: 'center', + marginLeft: theme.spacing(0.5), + }), + labelInput: css({ + width: '175px', + marginBottom: `-${theme.spacing(1)}`, + '& + &': { + marginLeft: theme.spacing(1), + }, + }), + labelsContainer: css({ + marginBottom: theme.spacing(3), + }), }; }; diff --git a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx index 8f141328cc5..66389b91baf 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -1,7 +1,10 @@ +import { css } from '@emotion/css'; import React from 'react'; import { useFormContext } from 'react-hook-form'; -import { Icon, Text, Stack } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { Icon, RadioButtonGroup, Stack, Text, useStyles2 } from '@grafana/ui'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; @@ -9,80 +12,53 @@ import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import LabelsField from './LabelsField'; import { NeedHelpInfo } from './NeedHelpInfo'; import { RuleEditorSection } from './RuleEditorSection'; +import { SimplifiedRouting } from './alert-rule-form/simplifiedRouting/SimplifiedRouting'; import { NotificationPreview } from './notificaton-preview/NotificationPreview'; type NotificationsStepProps = { alertUid?: string; }; -export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { - const { watch } = useFormContext(); - const [type, labels, queries, condition, folder, alertName] = watch([ +enum RoutingOptions { + NotificationPolicy = 'notification policy', + ContactPoint = 'contact point', +} + +export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { + const { watch, setValue } = useFormContext(); + const styles = useStyles2(getStyles); + + const [type, labels, queries, condition, folder, alertName, manualRouting] = watch([ 'type', 'labels', 'queries', 'condition', 'folder', 'name', + 'manualRouting', ]); const dataSourceName = watch('dataSourceName') ?? GRAFANA_RULES_SOURCE_NAME; const shouldRenderPreview = type === RuleFormType.grafana; - const NotificationsStepDescription = () => { - return ( - - - Add custom labels to change the way your notifications are routed. - - - - <> - Firing alert rule instances are routed to notification policies based on matching labels. All alert - rules and instances, irrespective of their labels, match the default notification policy. If there are - no nested policies, or no nested policies match the labels in the alert rule or alert instance, then - the default notification policy is the matching policy. - - - - Read about notification routing. - - - - - <> - Custom labels change the way your notifications are routed. First, add labels to your alert rule and - then connect them to your notification policy by adding label matchers. - - - - Read about Labels and annotations. - - - - - } - title="Notification routing" - /> - - ); + const routingOptions = [ + { label: 'Manually select contact point', value: RoutingOptions.ContactPoint }, + { label: 'Auto-select contact point', value: RoutingOptions.NotificationPolicy }, + ]; + + const onRoutingOptionChange = (option: RoutingOptions) => { + setValue('manualRouting', option === RoutingOptions.ContactPoint); }; + const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false; + + const shouldAllowSimplifiedRouting = type === RuleFormType.grafana && simplifiedRoutingToggleEnabled; + return ( {type === RuleFormType.cloudRecording ? ( @@ -90,23 +66,198 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { Add labels to help you better manage your recording rules ) : ( - + shouldAllowSimplifiedRouting && ( + + Select who should receive a notification when an alert rule fires. + + ) )} } fullWidth > - {shouldRenderPreview && ( - + {shouldAllowSimplifiedRouting && ( +
+ Configure notifications + + Select who should receive a notification when an alert rule fires. + +
)} +
); + + /** + * This component is used to render the section body of the NotificationsStep, depending on the routing option selected. + * If simplified routing is not enabled, it will render the NotificationPreview component. + * If simplified routing is enabled, it will render the switch between the manual routing and the notification policy routing. + * + */ + function RuleEditorSectionBody() { + if (!shouldAllowSimplifiedRouting) { + return ( + <> + {shouldRenderPreview && ( + + )} + + ); + } + return ( + + + + + + + + {manualRouting ? ( +
+ +
+ ) : ( + shouldRenderPreview && ( + + ) + )} +
+ ); + } }; + +// Auxiliar components to build the texts and descriptions in the NotificationsStep +function NeedHelpInfoForNotificationPolicy() { + return ( + + + <> + Firing alert rule instances are routed to notification policies based on matching labels. All alert rules + and instances, irrespective of their labels, match the default notification policy. If there are no nested + policies, or no nested policies match the labels in the alert rule or alert instance, then the default + notification policy is the matching policy. + + + + Read about notification routing. + + + + + <> + Custom labels change the way your notifications are routed. First, add labels to your alert rule and then + connect them to your notification policy by adding label matchers. + + + + Read about Labels and annotations. + + + + + } + title="Notification routing" + /> + ); +} + +function NeedHelpInfoForContactpoint() { + return ( + + Select a contact point to notify all recipients in it. +
+
+ Notifications for firing alert instances are grouped based on folder and alert rule name. +
+ The waiting time until the initial notification is sent for a new group created by an incoming alert is 30 + seconds. +
+ The waiting time to send a batch of new alerts for that group after the first notification was sent is 5 + minutes. +
+ The waiting time to resend an alert after they have successfully been sent is 4 hours. +
+ Grouping and wait time values are defined in your default notification policy. + + } + // todo: update the link with the new documentation about simplified routing + externalLink="`https://grafana.com/docs/grafana/latest/alerting/fundamentals/notification-policies/notifications/`" + linkText="Read more about notifiying contact points" + title="Notify contact points" + /> + ); +} +interface NotificationsStepDescriptionProps { + manualRouting: boolean; +} + +export const RoutingOptionDescription = ({ manualRouting }: NotificationsStepDescriptionProps) => { + const styles = useStyles2(getStyles); + return ( +
+ + {manualRouting + ? 'Notifications for firing alerts are routed to a selected contact point.' + : 'Notifications for firing alerts are routed to contact points based on matching labels.'} + + {manualRouting ? : } +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + routingOptions: css({ + marginTop: theme.spacing(2), + width: 'fit-content', + }), + simplifiedRouting: css({ + display: 'flex', + flexDirection: 'column', + marginTop: theme.spacing(2), + }), + configureNotifications: css({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + marginTop: theme.spacing(2), + }), + notificationsOptionDescription: css({ + marginTop: theme.spacing(1), + display: 'flex', + flexDirection: 'row', + alignItems: 'baseline', + gap: theme.spacing(0.5), + }), +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/RecordingRulesNameSpaceAndGroupStep.tsx b/public/app/features/alerting/unified/components/rule-editor/RecordingRulesNameSpaceAndGroupStep.tsx index 04bc6d5e89e..ad1af3184cd 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RecordingRulesNameSpaceAndGroupStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RecordingRulesNameSpaceAndGroupStep.tsx @@ -7,7 +7,7 @@ import { GroupAndNamespaceFields } from './GroupAndNamespaceFields'; import { RuleEditorSection } from './RuleEditorSection'; export function RecordingRulesNameSpaceAndGroupStep() { - const { watch } = useFormContext(); + const { watch } = useFormContext(); const dataSourceName = watch('dataSourceName'); diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/ContactPointSelector.tsx new file mode 100644 index 00000000000..09fa89ce112 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/ContactPointSelector.tsx @@ -0,0 +1,65 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { AnyAction } from 'redux'; + +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { Alert, Field, LoadingPlaceholder, Select, Stack, useStyles2 } from '@grafana/ui'; +import { AlertManagerDataSource } from 'app/features/alerting/unified/utils/datasource'; + +import { ContactPointReceiverSummary } from '../../../contact-points/ContactPoints.v2'; +import { useContactPointsWithStatus } from '../../../contact-points/useContactPoints'; + +import { selectContactPoint } from './SimplifiedRouting'; + +export interface ContactPointSelectorProps { + alertManager: AlertManagerDataSource; + selectedReceiver?: string; + dispatch: React.Dispatch; +} +export function ContactPointSelector({ selectedReceiver, alertManager, dispatch }: ContactPointSelectorProps) { + const styles = useStyles2(getStyles); + const onChange = (value: SelectableValue) => { + dispatch(selectContactPoint({ receiver: value?.value, alertManager })); + }; + const { isLoading, error, contactPoints: receivers } = useContactPointsWithStatus(); + const options = receivers.map((receiver) => { + const integrations = receiver?.grafana_managed_receiver_configs; + const description = ; + + return { label: receiver.name, value: receiver.name, description }; + }); + + if (error) { + return ; + } + if (isLoading) { + return ; + } + + return ( + + +
+