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:
+

+ {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 (
-
-
-
-
-
-
-
- );
-}
-
-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/SimplifiedRouting.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRouting.tsx
index 5b804f8ec09..5b15982411e 100644
--- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRouting.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRouting.tsx
@@ -1,46 +1,14 @@
-import { css } from '@emotion/css';
-import { createAction, createReducer } from '@reduxjs/toolkit';
-import React, { useEffect, useReducer } from 'react';
+import React, { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
-import { GrafanaTheme2 } from '@grafana/data';
-import { Icon, Link, Stack, Text, useStyles2 } from '@grafana/ui';
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
-import {
- AlertManagerDataSource,
- getAlertManagerDataSourcesByPermission,
-} from 'app/features/alerting/unified/utils/datasource';
-import { createUrl } from 'app/features/alerting/unified/utils/url';
+import { getAlertManagerDataSourcesByPermission } from 'app/features/alerting/unified/utils/datasource';
-import { ContactPointSelector } from './ContactPointSelector';
-
-export interface AMContactPoint {
- alertManager: AlertManagerDataSource;
- selectedContactPoint?: string;
-}
-
-export const selectContactPoint = createAction<{ receiver: string | undefined; alertManager: AlertManagerDataSource }>(
- 'simplifiedRouting/selectContactPoint'
-);
-
-export const receiversReducer = createReducer([], (builder) => {
- builder.addCase(selectContactPoint, (state, action) => {
- const { receiver, alertManager } = action.payload;
- const newContactPoint: AMContactPoint = { selectedContactPoint: receiver, alertManager };
- const existingContactPoint = state.find((cp) => cp.alertManager.name === alertManager.name);
-
- if (existingContactPoint) {
- existingContactPoint.selectedContactPoint = receiver;
- } else {
- state.push(newContactPoint);
- }
- });
-});
+import { AlertManagerManualRouting } from './AlertManagerRouting';
export function SimplifiedRouting() {
- const { getValues, setValue } = useFormContext();
- const styles = useStyles2(getStyles);
+ const { getValues } = useFormContext();
const contactPointsInAlert = getValues('contactPoints');
const allAlertManagersByPermission = getAlertManagerDataSourcesByPermission('notification');
@@ -54,100 +22,37 @@ export function SimplifiedRouting() {
const alertManagersDataSourcesWithConfigAPI = alertManagersDataSources.filter((am) => am.hasConfigurationAPI);
- // we merge the selected contact points with the alert manager meta data
- const alertManagersWithSelectedContactPoints = alertManagersDataSourcesWithConfigAPI.map((am) => {
- const selectedContactPoint = contactPointsInAlert?.find((cp) => cp.alertManager === am.name);
- return { alertManager: am, selectedContactPoint: selectedContactPoint?.selectedContactPoint };
- });
+ // we merge the selected contact points data for each alert manager, with the alert manager meta data
+ const alertManagersWithSelectedContactPoints = useMemo(
+ () =>
+ alertManagersDataSourcesWithConfigAPI.map((am) => {
+ const selectedContactPoint = contactPointsInAlert ? contactPointsInAlert[am.name] : undefined;
+ return {
+ alertManager: am,
+ selectedContactPoint: selectedContactPoint?.selectedContactPoint ?? '',
+ routeSettings: {
+ muteTimeIntervals: selectedContactPoint?.muteTimeIntervals ?? [],
+ overrideGrouping: selectedContactPoint?.overrideGrouping ?? false,
+ groupBy: selectedContactPoint?.groupBy ?? [],
+ overrideTimings: selectedContactPoint?.overrideTimings ?? false,
+ groupWaitValue: selectedContactPoint?.groupWaitValue ?? '',
+ groupIntervalValue: selectedContactPoint?.groupIntervalValue ?? '',
+ repeatIntervalValue: selectedContactPoint?.repeatIntervalValue ?? '',
+ },
+ };
+ }),
+ [alertManagersDataSourcesWithConfigAPI, contactPointsInAlert]
+ );
- // use reducer to keep this alertManagersWithSelectedContactPoints in the state
- const [alertManagersWithCPState, dispatch] = useReducer(receiversReducer, alertManagersWithSelectedContactPoints);
-
- function getContactPointsForForm(alertManagersWithCP: AMContactPoint[]) {
- return alertManagersWithCP.map((am) => {
- return { alertManager: am.alertManager.name, selectedContactPoint: am.selectedContactPoint };
- });
- }
-
- // whenever we update the receiversState we have to update the form too
- useEffect(() => {
- const contactPointsForForm = getContactPointsForForm(alertManagersWithCPState);
- setValue('contactPoints', contactPointsForForm, { shouldValidate: false });
- }, [alertManagersWithCPState, setValue]);
-
- const shouldShowAM = true;
-
- return alertManagersWithCPState.map((alertManagerContactPoint, index) => {
+ return alertManagersWithSelectedContactPoints.map((alertManagerContactPoint, index) => {
return (
-
-
- {shouldShowAM && (
-
-
-
- {' '}
- Alert manager:
-

- {alertManagerContactPoint.alertManager.name}
-
-
-
- )}
-
-
-
-
-
-
-
-
+
+
+
);
});
}
-
-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),
- }),
-});
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointDetails.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointDetails.tsx
new file mode 100644
index 00000000000..251b6a6095e
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointDetails.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+
+import { Stack } from '@grafana/ui';
+
+import { ContactPointReceiverTitleRow } from '../../../../contact-points/ContactPoints';
+import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY } from '../../../../contact-points/useContactPoints';
+import { ReceiverConfigWithMetadata, getReceiverDescription } from '../../../../contact-points/utils';
+
+interface ContactPointDetailsProps {
+ receivers: ReceiverConfigWithMetadata[];
+}
+
+export const ContactPointDetails = ({ receivers }: ContactPointDetailsProps) => {
+ return (
+
+
+ {receivers.map((receiver, index) => {
+ const metadata = receiver[RECEIVER_META_KEY];
+ const pluginMetadata = receiver[RECEIVER_PLUGIN_META_KEY];
+ const key = metadata.name + index;
+ return (
+
+ );
+ })}
+
+
+ );
+};
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx
new file mode 100644
index 00000000000..4e9e9cf5948
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx
@@ -0,0 +1,83 @@
+import { css } from '@emotion/css';
+import React from 'react';
+import { useFormContext } from 'react-hook-form';
+
+import { GrafanaTheme2, SelectableValue } from '@grafana/data';
+import { ActionMeta, Field, FieldValidationMessage, InputControl, Select, Stack, useStyles2 } from '@grafana/ui';
+import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
+
+import { ContactPointReceiverSummary } from '../../../../contact-points/ContactPoints';
+import { ContactPointWithMetadata } from '../../../../contact-points/utils';
+
+export interface ContactPointSelectorProps {
+ alertManager: string;
+ contactPoints: ContactPointWithMetadata[];
+ onSelectContactPoint: (contactPoint?: ContactPointWithMetadata) => void;
+}
+export function ContactPointSelector({ alertManager, contactPoints, onSelectContactPoint }: ContactPointSelectorProps) {
+ const styles = useStyles2(getStyles);
+ const {
+ register,
+ control,
+ formState: { errors },
+ watch,
+ } = useFormContext();
+
+ const options = contactPoints.map((receiver) => {
+ const integrations = receiver?.grafana_managed_receiver_configs;
+ const description = ;
+
+ return { label: receiver.name, value: receiver, description };
+ });
+
+ const selectedContactPointWithMetadata = options.find(
+ (option) => option.value.name === watch(`contactPoints.${alertManager}.selectedContactPoint`)
+ )?.value;
+ const selectedContactPointSelectableValue = selectedContactPointWithMetadata
+ ? { value: selectedContactPointWithMetadata, label: selectedContactPointWithMetadata.name }
+ : undefined;
+
+ return (
+
+
+ (
+ <>
+
+
+ {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;