From f6f259f0b505e83614b6066f8443885914a44921 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Tue, 2 Jan 2024 08:43:31 +0100 Subject: [PATCH] Alerting: Simplified routing part2 (#78671) * wip * WIP: Added some actions, timings and grouping * WIP: remove reducer and use form fields instead * Show defaults when using override in route settings * Update alert rule model for simplified routing * Use defaults in placeholders when overriding timings * Add validation for contact point * Add selected contact point details * Refactor: extract components to separate files and reorg in subfolders * Fix test * Update revalidate mode in form * Extract RuleEditorSectionBody outside NotificationStep component to avoid unmounting any time this one renders * Remove reValidateMode option in form * Fix after merging * Address PR review comments part1 * Address PR review comments part2 * Create routeTimingsFields constant to reuse labels, descriptions and arial labels for the mute timing fields * Move conditional rendering to the parent in AutomaticRooting and ManualAndAutomaticRouting * Simplify AlertManagerManualRouting properties --- .../contact-points/ContactPoints.tsx | 81 +++++---- .../EditNotificationPolicyForm.tsx | 33 ++-- .../routeTimingsFields.ts | 19 ++ .../rule-editor/NotificationsStep.tsx | 164 +++++++++--------- .../simplifiedRouting/AlertManagerRouting.tsx | 119 +++++++++++++ .../ContactPointSelector.tsx | 65 ------- .../simplifiedRouting/SimplifiedRouting.tsx | 161 ++++------------- .../contactPoint/ContactPointDetails.tsx | 34 ++++ .../contactPoint/ContactPointSelector.tsx | 83 +++++++++ .../route-settings/MuteTimingFields.tsx | 45 +++++ .../route-settings/RouteSettings.tsx | 108 ++++++++++++ .../route-settings/RouteTimings.tsx | 74 ++++++++ .../alerting/unified/types/rule-form.ts | 21 ++- .../__snapshots__/rule-form.test.ts.snap | 2 + .../alerting/unified/utils/rule-form.ts | 31 +++- public/app/types/unified-alerting-dto.ts | 2 + 16 files changed, 709 insertions(+), 333 deletions(-) create mode 100644 public/app/features/alerting/unified/components/notification-policies/routeTimingsFields.ts create mode 100644 public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx delete 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/contactPoint/ContactPointDetails.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx create mode 100644 public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteTimings.tsx diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx index 368cd23aa5f..e8bb3de0935 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx @@ -9,21 +9,21 @@ import { useToggle } from 'react-use'; import { dateTime, GrafanaTheme2 } from '@grafana/data'; import { - Text, - LinkButton, - TabsBar, - TabContent, - Tab, - Pagination, - Button, - Stack, Alert, - LoadingPlaceholder, - useStyles2, - Menu, + Button, Dropdown, - Tooltip, Icon, + LinkButton, + LoadingPlaceholder, + Menu, + Pagination, + Stack, + Tab, + TabContent, + TabsBar, + Text, + Tooltip, + useStyles2, } from '@grafana/ui'; import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap'; import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts'; @@ -455,35 +455,56 @@ const ContactPointReceiver = (props: ContactPointReceiverProps) => { const { name, type, description, diagnostics, pluginMetadata, sendingResolved = true } = props; const styles = useStyles2(getStyles); - const iconName = INTEGRATION_ICONS[type]; const hasMetadata = diagnostics !== undefined; return (
- - - {iconName && } - {pluginMetadata ? ( - - ) : ( - - {name} - - )} - - {description && ( - - {description} - - )} - + {hasMetadata && }
); }; +export interface ContactPointReceiverTitleRowProps { + name: string; + type: GrafanaNotifierType | string; + description?: ReactNode; + pluginMetadata?: ReceiverPluginMetadata; +} + +export function ContactPointReceiverTitleRow(props: ContactPointReceiverTitleRowProps) { + const { name, type, description, pluginMetadata } = props; + + const iconName = INTEGRATION_ICONS[type]; + + return ( + + + {iconName && } + {pluginMetadata ? ( + + ) : ( + + {name} + + )} + + {description && ( + + {description} + + )} + + ); +} + interface ContactPointReceiverMetadata { sendingResolved: boolean; diagnostics: NotifierStatus; diff --git a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx index a18c85dcd91..d0f3e8c8b31 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx @@ -3,20 +3,20 @@ import React, { ReactNode, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { + Badge, Button, Field, FieldArray, + FieldValidationMessage, Form, IconButton, Input, InputControl, MultiSelect, Select, + Stack, Switch, useStyles2, - Badge, - FieldValidationMessage, - Stack, } from '@grafana/ui'; import { MatcherOperator, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; @@ -25,20 +25,21 @@ import { FormAmRoute } from '../../types/amroutes'; import { SupportedPlugin } from '../../types/pluginBridges'; import { matcherFieldOptions } from '../../utils/alertmanager'; import { + amRouteToFormAmRoute, + commonGroupByOptions, emptyArrayFieldMatcher, mapMultiSelectValueToStrings, mapSelectValueToString, - stringToSelectableValue, - stringsToSelectableValues, - commonGroupByOptions, - amRouteToFormAmRoute, promDurationValidator, repeatIntervalValidator, + stringToSelectableValue, + stringsToSelectableValues, } from '../../utils/amroutes'; import { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types'; import { PromDurationInput } from './PromDurationInput'; import { getFormStyles } from './formStyles'; +import { routeTimingsFields } from './routeTimingsFields'; export interface AmRoutesExpandedFormProps { receivers: AmRouteReceiver[]; @@ -226,32 +227,32 @@ export const AmRoutesExpandedForm = ({ {watch().overrideTimings && ( <> @@ -262,7 +263,7 @@ export const AmRoutesExpandedForm = ({ return repeatIntervalValidator(value, groupInterval); }, })} - aria-label="Repeat interval value" + aria-label={routeTimingsFields.repeatInterval.ariaLabel} className={formStyles.promDurationInput} /> diff --git a/public/app/features/alerting/unified/components/notification-policies/routeTimingsFields.ts b/public/app/features/alerting/unified/components/notification-policies/routeTimingsFields.ts new file mode 100644 index 00000000000..159fd419f6d --- /dev/null +++ b/public/app/features/alerting/unified/components/notification-policies/routeTimingsFields.ts @@ -0,0 +1,19 @@ +export const routeTimingsFields = { + groupWait: { + label: 'Group wait', + description: + 'The waiting time until the initial notification is sent for a new group created by an incoming alert. If empty it will be inherited from the parent policy.', + ariaLabel: 'Group wait value', + }, + groupInterval: { + label: 'Group interval', + description: + 'The waiting time to send a batch of new alerts for that group after the first notification was sent. If empty it will be inherited from the parent policy.', + ariaLabel: 'Group interval value', + }, + repeatInterval: { + label: 'Repeat interval', + description: 'The waiting time to resend an alert after they have successfully been sent.', + ariaLabel: 'Repeat interval value', + }, +}; 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 66389b91baf..1a87e3335ee 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -25,34 +25,14 @@ enum RoutingOptions { } export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { - const { watch, setValue } = useFormContext(); + const { watch } = useFormContext(); const styles = useStyles2(getStyles); - const [type, labels, queries, condition, folder, alertName, manualRouting] = watch([ - 'type', - 'labels', - 'queries', - 'condition', - 'folder', - 'name', - 'manualRouting', - ]); + const [type] = watch(['type', 'labels', 'queries', 'condition', 'folder', 'name', 'manualRouting']); const dataSourceName = watch('dataSourceName') ?? GRAFANA_RULES_SOURCE_NAME; - - const shouldRenderPreview = type === RuleFormType.grafana; - - 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 shouldRenderpreview = type === RuleFormType.grafana; const shouldAllowSimplifiedRouting = type === RuleFormType.grafana && simplifiedRoutingToggleEnabled; return ( @@ -85,67 +65,86 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { )} - + {shouldAllowSimplifiedRouting ? ( // when simplified routing is enabled and is grafana rule + + ) : // when simplified routing is not enabled, render the notification preview as we did before + shouldRenderpreview ? ( + + ) : null} ); - - /** - * 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 && ( - - ) - )} -
- ); - } }; +/** + * Preconditions: + * - simplified routing is enabled + * - the alert rule is a grafana rule + * + * This component will render the switch between the manual routing and the notification policy routing. + * It also renders the section body of the NotificationsStep, depending on the routing option selected. + * If manual routing is selected, it will render the SimplifiedRouting component. + * If notification policy routing is selected, it will render the AutomaticRouting component. + * + */ +function ManualAndAutomaticRouting({ alertUid }: { alertUid?: string }) { + const { watch, setValue } = useFormContext(); + const styles = useStyles2(getStyles); + + const [manualRouting] = watch(['manualRouting']); + + 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); + }; + + return ( + + + + + + + + {manualRouting ? : } + + ); +} + +interface AutomaticRootingProps { + alertUid?: string; +} + +function AutomaticRooting({ alertUid }: AutomaticRootingProps) { + const { watch } = useFormContext(); + const [labels, queries, condition, folder, alertName] = watch([ + 'labels', + 'queries', + 'condition', + 'folder', + 'name', + 'manualRouting', + ]); + return ( + + ); +} + // Auxiliar components to build the texts and descriptions in the NotificationsStep function NeedHelpInfoForNotificationPolicy() { return ( @@ -242,11 +241,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ marginTop: theme.spacing(2), width: 'fit-content', }), - simplifiedRouting: css({ - display: 'flex', - flexDirection: 'column', - marginTop: theme.spacing(2), - }), configureNotifications: css({ display: 'flex', flexDirection: 'column', diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx new file mode 100644 index 00000000000..436c892861a --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx @@ -0,0 +1,119 @@ +import { css } from '@emotion/css'; +import React, { useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Alert, CollapsableSection, Icon, Link, LoadingPlaceholder, Stack, Text, useStyles2 } from '@grafana/ui'; +import { AlertManagerDataSource } from 'app/features/alerting/unified/utils/datasource'; +import { createUrl } from 'app/features/alerting/unified/utils/url'; + +import { useContactPointsWithStatus } from '../../../contact-points/useContactPoints'; +import { ContactPointWithMetadata } from '../../../contact-points/utils'; + +import { ContactPointDetails } from './contactPoint/ContactPointDetails'; +import { ContactPointSelector } from './contactPoint/ContactPointSelector'; +import { MuteTimingFields } from './route-settings/MuteTimingFields'; +import { RoutingSettings } from './route-settings/RouteSettings'; + +interface AlertManagerManualRoutingProps { + alertManager: AlertManagerDataSource; +} + +export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRoutingProps) { + const styles = useStyles2(getStyles); + + const alertManagerName = alertManager.name; + const { isLoading, error: errorInContactPointStatus, contactPoints } = useContactPointsWithStatus(); + const shouldShowAM = true; + const [selectedContactPointWithMetadata, setSelectedContactPointWithMetadata] = useState< + ContactPointWithMetadata | undefined + >(); + + if (errorInContactPointStatus) { + return ; + } + if (isLoading) { + return ; + } + return ( + + {shouldShowAM && ( + +
+
+ Alert manager: + Alert manager logo + {alertManagerName} +
+
+
+ )} + + + + + {selectedContactPointWithMetadata?.grafana_managed_receiver_configs && ( + + )} +
+ + + + + + +
+
+ ); +} +function LinkToContactPoints() { + const hrefToContactPoints = '/alerting/notifications'; + return ( + + + To browse contact points and create new ones go to + Contact points + + + + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + firstAlertManagerLine: css({ + height: 1, + width: theme.spacing(4), + backgroundColor: theme.colors.secondary.main, + }), + alertManagerName: css({ + with: 'fit-content', + }), + secondAlertManagerLine: css({ + height: '1px', + width: '100%', + flex: 1, + backgroundColor: theme.colors.secondary.main, + }), + img: css({ + marginLeft: theme.spacing(2), + width: theme.spacing(3), + height: theme.spacing(3), + marginRight: theme.spacing(1), + }), + collapsableSection: css({ + width: 'fit-content', + fontSize: theme.typography.body.fontSize, + }), + routingSection: css({ + display: 'flex', + flexDirection: 'column', + maxWidth: theme.breakpoints.values.xl, + border: `solid 1px ${theme.colors.border.weak}`, + borderRadius: theme.shape.radius.default, + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + marginTop: theme.spacing(2), + }), +}); 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 deleted file mode 100644 index f55648c5ae0..00000000000 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/ContactPointSelector.tsx +++ /dev/null @@ -1,65 +0,0 @@ -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'; -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 ( - - -
- , _: ActionMeta) => { + onChange(value?.value?.name); + onSelectContactPoint(value?.value); + }} + // We are passing a JSX.Element into the "description" for options, which isn't how the TS typings are defined. + // The regular Select component will render it just fine, but we can't update the typings because SelectableValue + // is shared with other components where the "description" _has_ to be a string. + // I've tried unsuccessfully to separate the typings just I'm giving up :'( + // @ts-ignore + options={options} + width={50} + /> +
+ {error && {'Contact point is required.'}} + + )} + control={control} + name={`contactPoints.${alertManager}.selectedContactPoint`} + /> +
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + contactPointsSelector: css({ + marginTop: theme.spacing(1), + }), +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx new file mode 100644 index 00000000000..b5598ecb6e4 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; + +import { Field, InputControl, MultiSelect, useStyles2 } from '@grafana/ui'; +import { useMuteTimingOptions } from 'app/features/alerting/unified/hooks/useMuteTimingOptions'; +import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; +import { mapMultiSelectValueToStrings } from 'app/features/alerting/unified/utils/amroutes'; + +import { getFormStyles } from '../../../../notification-policies/formStyles'; + +export interface MuteTimingFieldsProps { + alertManager: string; +} + +export function MuteTimingFields({ alertManager }: MuteTimingFieldsProps) { + const styles = useStyles2(getFormStyles); + const { + control, + formState: { errors }, + } = useFormContext(); + + const muteTimingOptions = useMuteTimingOptions(); + return ( + + ( + onChange(mapMultiSelectValueToStrings(value))} + options={muteTimingOptions} + /> + )} + control={control} + name={`contactPoints.${alertManager}.muteTimeIntervals`} + /> + + ); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx new file mode 100644 index 00000000000..972cfcff40e --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import { useFormContext } from 'react-hook-form'; + +import { Field, FieldValidationMessage, InputControl, MultiSelect, Stack, Switch, Text, useStyles2 } from '@grafana/ui'; +import { useAlertmanagerConfig } from 'app/features/alerting/unified/hooks/useAlertmanagerConfig'; +import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext'; +import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; +import { + commonGroupByOptions, + mapMultiSelectValueToStrings, + stringToSelectableValue, + stringsToSelectableValues, +} from 'app/features/alerting/unified/utils/amroutes'; + +import { getFormStyles } from '../../../../notification-policies/formStyles'; +import { TIMING_OPTIONS_DEFAULTS } from '../../../../notification-policies/timingOptions'; + +import { RouteTimings } from './RouteTimings'; + +export interface RoutingSettingsProps { + alertManager: string; +} +export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => { + const formStyles = useStyles2(getFormStyles); + const { + control, + watch, + register, + formState: { errors }, + } = useFormContext(); + const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues([])); + const { groupBy, groupIntervalValue, groupWaitValue, repeatIntervalValue } = useGetDefaultsForRoutingSettings(); + const overrideGrouping = watch(`contactPoints.${alertManager}.overrideGrouping`); + const overrideTimings = watch(`contactPoints.${alertManager}.overrideTimings`); + return ( + + + + + + {!overrideGrouping && ( + + Grouping: {groupBy.join(', ')} + + )} + + {overrideGrouping && ( + + ( + <> + { + setGroupByOptions((opts) => [...opts, stringToSelectableValue(opt)]); + + // @ts-ignore-check: react-hook-form made me do this + setValue(`contactPoints.${alertManager}.groupBy`, [...field.value, opt]); + }} + onChange={(value) => onChange(mapMultiSelectValueToStrings(value))} + options={[...commonGroupByOptions, ...groupByOptions]} + /> + {error && {'At least one group by option is required'}} + + )} + name={`contactPoints.${alertManager}.groupBy`} + control={control} + /> + + )} + + + + + {!overrideTimings && ( + + Group wait: {groupWaitValue}, + Group interval: {groupIntervalValue}, + Repeat interval: {repeatIntervalValue} + + )} + + {overrideTimings && } + + ); +}; + +function useGetDefaultsForRoutingSettings() { + const { selectedAlertmanager } = useAlertmanager(); + const { currentData } = useAlertmanagerConfig(selectedAlertmanager); + const config = currentData?.alertmanager_config; + return React.useMemo(() => { + return { + groupWaitValue: TIMING_OPTIONS_DEFAULTS.group_wait, + groupIntervalValue: TIMING_OPTIONS_DEFAULTS.group_interval, + repeatIntervalValue: TIMING_OPTIONS_DEFAULTS.repeat_interval, + groupBy: config?.route?.group_by ?? [], + }; + }, [config]); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteTimings.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteTimings.tsx new file mode 100644 index 00000000000..02eb7e4ede4 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteTimings.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; + +import { Field, useStyles2 } from '@grafana/ui'; +import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; +import { promDurationValidator, repeatIntervalValidator } from 'app/features/alerting/unified/utils/amroutes'; + +import { PromDurationInput } from '../../../../notification-policies/PromDurationInput'; +import { getFormStyles } from '../../../../notification-policies/formStyles'; +import { routeTimingsFields } from '../../../../notification-policies/routeTimingsFields'; +import { TIMING_OPTIONS_DEFAULTS } from '../../../../notification-policies/timingOptions'; + +interface RouteTimingsProps { + alertManager: string; +} + +export function RouteTimings({ alertManager }: RouteTimingsProps) { + const formStyles = useStyles2(getFormStyles); + const { + register, + formState: { errors }, + getValues, + } = useFormContext(); + return ( + <> + + + + + + + + { + const groupInterval = getValues(`contactPoints.${alertManager}.repeatIntervalValue`); + return repeatIntervalValidator(value, groupInterval); + }, + })} + aria-label={routeTimingsFields.repeatInterval.ariaLabel} + className={formStyles.promDurationInput} + placeholder={TIMING_OPTIONS_DEFAULTS.repeat_interval} + /> + + + ); +} diff --git a/public/app/features/alerting/unified/types/rule-form.ts b/public/app/features/alerting/unified/types/rule-form.ts index b02cd19940c..e4c787f7e21 100644 --- a/public/app/features/alerting/unified/types/rule-form.ts +++ b/public/app/features/alerting/unified/types/rule-form.ts @@ -8,9 +8,20 @@ export enum RuleFormType { cloudRecording = 'cloud-recording', } -export interface ContactPoints { - alertManager: string; - selectedContactPoint?: string; +export interface ContactPoint { + selectedContactPoint: string; + overrideGrouping: boolean; + groupBy: string[]; + overrideTimings: boolean; + groupWaitValue: string; + groupIntervalValue: string; + repeatIntervalValue: string; + muteTimeIntervals: string[]; +} + +// key: name of alert manager, value ContactPoint +export interface AlertManagerManualRouting { + [key: string]: ContactPoint; } export interface RuleFormValues { @@ -32,8 +43,8 @@ export interface RuleFormValues { evaluateEvery: string; evaluateFor: string; isPaused?: boolean; - contactPoints?: ContactPoints[]; - manualRouting: boolean; + manualRouting: boolean; // if true contactPoints are used. This field will not be used for saving the rule + contactPoints?: AlertManagerManualRouting; // cortex / loki rules namespace: string; diff --git a/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap b/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap index cde06160d02..d41c9d98a82 100644 --- a/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap +++ b/public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap @@ -10,6 +10,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form valu "for": "5m", "grafana_alert": { "condition": "A", + "contactPoints": undefined, "data": [], "exec_err_state": "Error", "is_paused": false, @@ -32,6 +33,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should not save both instant and range "for": "5m", "grafana_alert": { "condition": "A", + "contactPoints": undefined, "data": [ { "datasourceUid": "dsuid", diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 26d2c1d7c98..963e78e4502 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -68,7 +68,10 @@ export const getDefaultFormValues = (): RuleFormValues => { evaluateFor: '5m', evaluateEvery: MINUTE, manualRouting: false, // let's decide this later - contactPoints: [], + contactPoints: {}, + overrideGrouping: false, + overrideTimings: false, + muteTimeIntervals: [], // cortex / loki namespace: '', @@ -136,7 +139,8 @@ export function normalizeDefaultAnnotations(annotations: Array<{ key: string; va } export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): PostableRuleGrafanaRuleDTO { - const { name, condition, noDataState, execErrState, evaluateFor, queries, isPaused } = values; + const { name, condition, noDataState, execErrState, evaluateFor, queries, isPaused, contactPoints, manualRouting } = + values; if (condition) { return { grafana_alert: { @@ -146,6 +150,7 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl exec_err_state: execErrState, data: queries.map(fixBothInstantAndRangeQuery), is_paused: Boolean(isPaused), + contactPoints: manualRouting ? contactPoints : undefined, }, for: evaluateFor, annotations: arrayToRecord(values.annotations || []), @@ -178,8 +183,26 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF labels: listifyLabelsOrAnnotations(rule.labels, true), folder: { title: namespace, uid: ga.namespace_uid }, isPaused: ga.is_paused, - // manualrouting: ?? //todo depending on the implementation of the manual routing - // contactPoints: ?? //todo depending on the implementation of the manual routing + contactPoints: ga.contactPoints, + manualRouting: Boolean(ga.contactPoints), + // next line is for testing + // manualRouting: true, + // contactPoints: { + // grafana: { + // selectedContactPoint: "contact_point_5", + // muteTimeIntervals: [ + // "mute timing 1" + // ], + // overrideGrouping: true, + // overrideTimings: true, + // "groupBy": [ + // "..." + // ], + // groupWaitValue: "35s", + // groupIntervalValue: "6m", + // repeatIntervalValue: "5h" + // } + // } }; } else { throw new Error('Unexpected type of rule for grafana rules source'); diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index fede7ab2d9c..98f7f526244 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -1,6 +1,7 @@ // Prometheus API DTOs, possibly to be autogenerated from openapi spec in the near future import { DataQuery, RelativeTimeRange } from '@grafana/data'; +import { AlertManagerManualRouting } from 'app/features/alerting/unified/types/rule-form'; import { AlertGroupTotals } from './unified-alerting'; @@ -205,6 +206,7 @@ export interface PostableGrafanaRuleDefinition { exec_err_state: GrafanaAlertStateDecision; data: AlertQuery[]; is_paused?: boolean; + contactPoints?: AlertManagerManualRouting; } export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition { id?: string;