From 6f8ddac4eb425810fd04cc9b98ef904473be8404 Mon Sep 17 00:00:00 2001
From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com>
Date: Tue, 9 Jan 2024 08:39:43 +0100
Subject: [PATCH] Alerting: Simplified routing part3 (#79941)
* Update alert rule model in FE following BE design doc
* Remove unnecessary conditional rendering
* Update styles for optional route settings: add indentation
* Update test
* Add validation for grouBy to include grafana_folder and alertname
* Split conversions between FEdataModel/ DTO, in separate functions
* Update texts following Brenda's suggestions
* Update text
---
.../rule-editor/NotificationsStep.tsx | 10 +-
.../simplifiedRouting/AlertManagerRouting.tsx | 29 ++--
.../route-settings/MuteTimingFields.tsx | 3 +-
.../route-settings/RouteSettings.tsx | 57 +++++++-
.../__snapshots__/rule-form.test.ts.snap | 4 +-
.../alerting/unified/utils/rule-form.test.ts | 126 +++++++++++++++++-
.../alerting/unified/utils/rule-form.ts | 60 ++++++++-
public/app/types/unified-alerting-dto.ts | 11 +-
8 files changed, 262 insertions(+), 38 deletions(-)
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:
-

- {alertManagerName}
-
-
-
- )}
+
+
+
+ Alert manager:
+

+ {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;