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 1a87e3335ee..f724766a65b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -80,9 +80,9 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { * - 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. + * This component will render the switch between the select contact point 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 select contact point routing is selected, it will render the SimplifiedRouting component. * If notification policy routing is selected, it will render the AutomaticRouting component. * */ @@ -93,8 +93,8 @@ function ManualAndAutomaticRouting({ alertUid }: { alertUid?: string }) { const [manualRouting] = watch(['manualRouting']); const routingOptions = [ - { label: 'Manually select contact point', value: RoutingOptions.ContactPoint }, - { label: 'Auto-select contact point', value: RoutingOptions.NotificationPolicy }, + { label: 'Select contact point', value: RoutingOptions.ContactPoint }, + { label: 'Use notification policy', value: RoutingOptions.NotificationPolicy }, ]; const onRoutingOptionChange = (option: RoutingOptions) => { @@ -229,7 +229,7 @@ export const RoutingOptionDescription = ({ manualRouting }: NotificationsStepDes {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.'} + : 'Notifications for firing alerts are routed to contact points based on matching labels and the notification policy tree.'} {manualRouting ? : } 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 index 436c892861a..61662f9ff82 100644 --- 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 @@ -23,7 +23,6 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo const alertManagerName = alertManager.name; const { isLoading, error: errorInContactPointStatus, contactPoints } = useContactPointsWithStatus(); - const shouldShowAM = true; const [selectedContactPointWithMetadata, setSelectedContactPointWithMetadata] = useState< ContactPointWithMetadata | undefined >(); @@ -36,17 +35,15 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo } return ( - {shouldShowAM && ( - -
-
- Alert manager: - Alert manager logo - {alertManagerName} -
-
-
- )} + +
+
+ Alert manager: + Alert manager logo + {alertManagerName} +
+
+
)}
- + @@ -74,7 +75,7 @@ function LinkToContactPoints() { return ( - To browse contact points and create new ones go to + To browse contact points and create new ones, go to Contact points 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 index b5598ecb6e4..70728a041d7 100644 --- 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 @@ -24,7 +24,7 @@ export function MuteTimingFields({ alertManager }: MuteTimingFieldsProps) { onChange(mapMultiSelectValueToStrings(value))} options={muteTimingOptions} + placeholder="Select mute timings..." /> )} control={control} 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 index 972cfcff40e..0b20c40e2df 100644 --- 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 @@ -1,7 +1,19 @@ +import { css } from '@emotion/css'; import React, { useState } from 'react'; import { useFormContext } from 'react-hook-form'; -import { Field, FieldValidationMessage, InputControl, MultiSelect, Stack, Switch, Text, useStyles2 } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { + Field, + FieldValidationMessage, + InlineField, + 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'; @@ -32,12 +44,14 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => { const { groupBy, groupIntervalValue, groupWaitValue, repeatIntervalValue } = useGetDefaultsForRoutingSettings(); const overrideGrouping = watch(`contactPoints.${alertManager}.overrideGrouping`); const overrideTimings = watch(`contactPoints.${alertManager}.overrideTimings`); + const requiredFieldsInGroupBy = ['grafana_folder', 'alertname']; + const styles = useStyles2(getStyles); return ( - + - + {!overrideGrouping && ( Grouping: {groupBy.join(', ')} @@ -50,8 +64,22 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => { description="Group alerts when you receive a notification based on labels. If empty it will be inherited from the default notification policy." {...register(`contactPoints.${alertManager}.groupBy`, { required: true })} invalid={!!errors.contactPoints?.[alertManager]?.groupBy} + className={styles.optionalContent} > { + if (!value || value.length === 0) { + return 'At least one group by option is required.'; + } + // we need to make sure that the required fields are included + const requiredFieldsIncluded = requiredFieldsInGroupBy.every((field) => value.includes(field)); + if (!requiredFieldsIncluded) { + return `Group by must include ${requiredFieldsInGroupBy.join(', ')}`; + } + return true; + }, + }} render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => ( <> { onChange={(value) => onChange(mapMultiSelectValueToStrings(value))} options={[...commonGroupByOptions, ...groupByOptions]} /> - {error && {'At least one group by option is required'}} + {error && {error.message}} )} name={`contactPoints.${alertManager}.groupBy`} @@ -77,9 +105,9 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => { )} - + - + {!overrideTimings && ( Group wait: {groupWaitValue}, @@ -88,7 +116,11 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => { )} - {overrideTimings && } + {overrideTimings && ( +
+ +
+ )}
); }; @@ -106,3 +138,14 @@ function useGetDefaultsForRoutingSettings() { }; }, [config]); } + +const getStyles = (theme: GrafanaTheme2) => ({ + switchElement: css({ + flexFlow: 'row-reverse', + gap: theme.spacing(1), + alignItems: 'center', + }), + optionalContent: css({ + marginLeft: '49px', + }), +}); 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 d41c9d98a82..feca637e835 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,11 +10,11 @@ exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form valu "for": "5m", "grafana_alert": { "condition": "A", - "contactPoints": undefined, "data": [], "exec_err_state": "Error", "is_paused": false, "no_data_state": "NoData", + "notification_settings": undefined, "title": "", }, "labels": { @@ -33,7 +33,6 @@ exports[`formValuesToRulerGrafanaRuleDTO should not save both instant and range "for": "5m", "grafana_alert": { "condition": "A", - "contactPoints": undefined, "data": [ { "datasourceUid": "dsuid", @@ -54,6 +53,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should not save both instant and range "exec_err_state": "Error", "is_paused": false, "no_data_state": "NoData", + "notification_settings": undefined, "title": "", }, "labels": { diff --git a/public/app/features/alerting/unified/utils/rule-form.test.ts b/public/app/features/alerting/unified/utils/rule-form.test.ts index 5ac93c82d7e..4d1324e9c97 100644 --- a/public/app/features/alerting/unified/utils/rule-form.test.ts +++ b/public/app/features/alerting/unified/utils/rule-form.test.ts @@ -1,13 +1,16 @@ import { PromQuery } from 'app/plugins/datasource/prometheus/types'; -import { RulerAlertingRuleDTO } from 'app/types/unified-alerting-dto'; +import { GrafanaAlertStateDecision, GrafanaRuleDefinition, RulerAlertingRuleDTO } from 'app/types/unified-alerting-dto'; -import { RuleFormType, RuleFormValues } from '../types/rule-form'; +import { AlertManagerManualRouting, RuleFormType, RuleFormValues } from '../types/rule-form'; +import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; import { alertingRulerRuleToRuleForm, formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, + getContactPointsFromDTO, getDefaultFormValues, + getNotificationSettingsForDTO, } from './rule-form'; describe('formValuesToRulerGrafanaRuleDTO', () => { @@ -85,3 +88,122 @@ describe('formValuesToRulerGrafanaRuleDTO', () => { expect(alertingRulerRuleToRuleForm(rule)).toMatchSnapshot(); }); }); +describe('getContactPointsFromDTO', () => { + it('should return undefined if notification_settings is not defined', () => { + const ga: GrafanaRuleDefinition = { + uid: '123', + title: 'myalert', + namespace_uid: '123', + condition: 'A', + no_data_state: GrafanaAlertStateDecision.Alerting, + exec_err_state: GrafanaAlertStateDecision.Alerting, + data: [ + { + datasourceUid: '123', + refId: 'A', + queryType: 'huh', + model: { refId: 'A' }, + }, + ], + notification_settings: undefined, + }; + + const result = getContactPointsFromDTO(ga); + expect(result).toBeUndefined(); + }); + + it('should return routingSettings with correct props if notification_settings is defined', () => { + const ga: GrafanaRuleDefinition = { + uid: '123', + title: 'myalert', + namespace_uid: '123', + condition: 'A', + no_data_state: GrafanaAlertStateDecision.Alerting, + exec_err_state: GrafanaAlertStateDecision.Alerting, + data: [ + { + datasourceUid: '123', + refId: 'A', + queryType: 'huh', + model: { refId: 'A' }, + }, + ], + notification_settings: { + receiver: 'receiver', + mute_timings: ['mute_timing'], + group_by: ['group_by'], + group_wait: 'group_wait', + group_interval: 'group_interval', + repeat_interval: 'repeat_interval', + }, + }; + + const result = getContactPointsFromDTO(ga); + expect(result).toEqual({ + [GRAFANA_RULES_SOURCE_NAME]: { + selectedContactPoint: 'receiver', + muteTimeIntervals: ['mute_timing'], + overrideGrouping: true, + overrideTimings: true, + groupBy: ['group_by'], + groupWaitValue: 'group_wait', + groupIntervalValue: 'group_interval', + repeatIntervalValue: 'repeat_interval', + }, + }); + }); +}); + +describe('getNotificationSettingsForDTO', () => { + it('should return undefined if manualRouting is false', () => { + const manualRouting = false; + const contactPoints: AlertManagerManualRouting = { + grafana: { + selectedContactPoint: 'receiver', + muteTimeIntervals: ['mute_timing'], + overrideGrouping: true, + overrideTimings: true, + groupBy: ['group_by'], + groupWaitValue: 'group_wait', + groupIntervalValue: 'group_interval', + repeatIntervalValue: 'repeat_interval', + }, + }; + + const result = getNotificationSettingsForDTO(manualRouting, contactPoints); + expect(result).toBeUndefined(); + }); + + it('should return undefined if selectedContactPoint is not defined', () => { + const manualRouting = true; + + const result = getNotificationSettingsForDTO(manualRouting, undefined); + expect(result).toBeUndefined(); + }); + + it('should return notification settings if manualRouting is true and selectedContactPoint is defined', () => { + const manualRouting = true; + const contactPoints: AlertManagerManualRouting = { + grafana: { + selectedContactPoint: 'receiver', + muteTimeIntervals: ['mute_timing'], + overrideGrouping: true, + overrideTimings: true, + groupBy: ['group_by'], + groupWaitValue: 'group_wait', + groupIntervalValue: 'group_interval', + repeatIntervalValue: 'repeat_interval', + }, + }; + + const result = getNotificationSettingsForDTO(manualRouting, contactPoints); + expect(result).toEqual({ + receiver: 'receiver', + mute_timings: ['mute_timing'], + group_by: ['group_by'], + group_wait: 'group_wait', + group_interval: 'group_interval', + repeat_interval: 'repeat_interval', + }); + }); +}); diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 963e78e4502..91b3a94ac6a 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -25,6 +25,8 @@ import { AlertQuery, Annotations, GrafanaAlertStateDecision, + GrafanaNotificationSettings, + GrafanaRuleDefinition, Labels, PostableRuleGrafanaRuleDTO, RulerAlertingRuleDTO, @@ -33,11 +35,11 @@ import { } from 'app/types/unified-alerting-dto'; import { EvalFunction } from '../../state/alertDef'; -import { RuleFormType, RuleFormValues } from '../types/rule-form'; +import { AlertManagerManualRouting, ContactPoint, RuleFormType, RuleFormValues } from '../types/rule-form'; import { getRulesAccess } from './access-control'; import { Annotation, defaultAnnotations } from './constants'; -import { getDefaultOrFirstCompatibleDataSource, isGrafanaRulesSource } from './datasource'; +import { getDefaultOrFirstCompatibleDataSource, GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from './datasource'; import { arrayToRecord, recordToArray } from './misc'; import { isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './rules'; import { parseInterval } from './time'; @@ -138,10 +140,34 @@ export function normalizeDefaultAnnotations(annotations: Array<{ key: string; va return orderedAnnotations; } +export function getNotificationSettingsForDTO( + manualRouting: boolean, + contactPoints?: AlertManagerManualRouting +): GrafanaNotificationSettings | undefined { + if (contactPoints?.grafana?.selectedContactPoint && manualRouting) { + return { + receiver: contactPoints?.grafana?.selectedContactPoint, + mute_timings: contactPoints?.grafana?.muteTimeIntervals, + group_by: contactPoints?.grafana?.overrideGrouping ? contactPoints?.grafana?.groupBy : undefined, + group_wait: contactPoints?.grafana?.overrideTimings ? contactPoints?.grafana?.groupWaitValue : undefined, + group_interval: contactPoints?.grafana?.overrideTimings ? contactPoints?.grafana?.groupIntervalValue : undefined, + repeat_interval: contactPoints?.grafana?.overrideTimings + ? contactPoints?.grafana?.repeatIntervalValue + : undefined, + }; + } + return undefined; +} + export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): PostableRuleGrafanaRuleDTO { const { name, condition, noDataState, execErrState, evaluateFor, queries, isPaused, contactPoints, manualRouting } = values; if (condition) { + const notificationSettings: GrafanaNotificationSettings | undefined = getNotificationSettingsForDTO( + manualRouting, + contactPoints + ); + return { grafana_alert: { title: name, @@ -150,7 +176,7 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl exec_err_state: execErrState, data: queries.map(fixBothInstantAndRangeQuery), is_paused: Boolean(isPaused), - contactPoints: manualRouting ? contactPoints : undefined, + notification_settings: notificationSettings, }, for: evaluateFor, annotations: arrayToRecord(values.annotations || []), @@ -160,6 +186,27 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl throw new Error('Cannot create rule without specifying alert condition'); } +export function getContactPointsFromDTO(ga: GrafanaRuleDefinition): AlertManagerManualRouting | undefined { + const contactPoint: ContactPoint | undefined = ga.notification_settings + ? { + selectedContactPoint: ga.notification_settings.receiver, + muteTimeIntervals: ga.notification_settings.mute_timings ?? [], + overrideGrouping: Boolean(ga.notification_settings?.group_by), + overrideTimings: Boolean(ga.notification_settings.group_wait), + groupBy: ga.notification_settings.group_by || [], + groupWaitValue: ga.notification_settings.group_wait || '', + groupIntervalValue: ga.notification_settings.group_interval || '', + repeatIntervalValue: ga.notification_settings.repeat_interval || '', + } + : undefined; + const routingSettings: AlertManagerManualRouting | undefined = contactPoint + ? { + [GRAFANA_RULES_SOURCE_NAME]: contactPoint, + } + : undefined; + return routingSettings; +} + export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleFormValues { const { ruleSourceName, namespace, group, rule } = ruleWithLocation; @@ -168,6 +215,8 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF if (isGrafanaRulerRule(rule)) { const ga = rule.grafana_alert; + const routingSettings: AlertManagerManualRouting | undefined = getContactPointsFromDTO(ga); + return { ...defaultFormValues, name: ga.title, @@ -183,8 +232,9 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF labels: listifyLabelsOrAnnotations(rule.labels, true), folder: { title: namespace, uid: ga.namespace_uid }, isPaused: ga.is_paused, - contactPoints: ga.contactPoints, - manualRouting: Boolean(ga.contactPoints), + + contactPoints: routingSettings, + manualRouting: Boolean(routingSettings), // next line is for testing // manualRouting: true, // contactPoints: { diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index 98f7f526244..b35fe160252 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -1,7 +1,6 @@ // 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'; @@ -198,6 +197,14 @@ export interface AlertQuery { model: AlertDataQuery; } +export interface GrafanaNotificationSettings { + receiver: string; + group_by?: string[]; + group_wait?: string; + group_interval?: string; + repeat_interval?: string; + mute_timings?: string[]; +} export interface PostableGrafanaRuleDefinition { uid?: string; title: string; @@ -206,7 +213,7 @@ export interface PostableGrafanaRuleDefinition { exec_err_state: GrafanaAlertStateDecision; data: AlertQuery[]; is_paused?: boolean; - contactPoints?: AlertManagerManualRouting; + notification_settings?: GrafanaNotificationSettings; } export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition { id?: string;