mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -06:00
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
This commit is contained in:
parent
ae3156d727
commit
f6f259f0b5
@ -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 (
|
||||
<div className={styles.integrationWrapper}>
|
||||
<Stack direction="column" gap={0.5}>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
{iconName && <Icon name={iconName} />}
|
||||
{pluginMetadata ? (
|
||||
<ReceiverMetadataBadge metadata={pluginMetadata} />
|
||||
) : (
|
||||
<Text variant="body" color="primary">
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
{description && (
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<ContactPointReceiverTitleRow
|
||||
name={name}
|
||||
type={type}
|
||||
description={description}
|
||||
pluginMetadata={pluginMetadata}
|
||||
/>
|
||||
{hasMetadata && <ContactPointReceiverMetadataRow diagnostics={diagnostics} sendingResolved={sendingResolved} />}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
{iconName && <Icon name={iconName} />}
|
||||
{pluginMetadata ? (
|
||||
<ReceiverMetadataBadge metadata={pluginMetadata} />
|
||||
) : (
|
||||
<Text variant="body" color="primary">
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
{description && (
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
interface ContactPointReceiverMetadata {
|
||||
sendingResolved: boolean;
|
||||
diagnostics: NotifierStatus;
|
||||
|
@ -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 && (
|
||||
<>
|
||||
<Field
|
||||
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."
|
||||
label={routeTimingsFields.groupWait.label}
|
||||
description={routeTimingsFields.groupWait.description}
|
||||
invalid={!!errors.groupWaitValue}
|
||||
error={errors.groupWaitValue?.message}
|
||||
>
|
||||
<PromDurationInput
|
||||
{...register('groupWaitValue', { validate: promDurationValidator })}
|
||||
aria-label="Group wait value"
|
||||
aria-label={routeTimingsFields.groupWait.ariaLabel}
|
||||
className={formStyles.promDurationInput}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
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."
|
||||
label={routeTimingsFields.groupInterval.label}
|
||||
description={routeTimingsFields.groupInterval.description}
|
||||
invalid={!!errors.groupIntervalValue}
|
||||
error={errors.groupIntervalValue?.message}
|
||||
>
|
||||
<PromDurationInput
|
||||
{...register('groupIntervalValue', { validate: promDurationValidator })}
|
||||
aria-label="Group interval value"
|
||||
aria-label={routeTimingsFields.groupInterval.ariaLabel}
|
||||
className={formStyles.promDurationInput}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Repeat interval"
|
||||
description="The waiting time to resend an alert after they have successfully been sent."
|
||||
label={routeTimingsFields.repeatInterval.label}
|
||||
description={routeTimingsFields.repeatInterval.description}
|
||||
invalid={!!errors.repeatIntervalValue}
|
||||
error={errors.repeatIntervalValue?.message}
|
||||
>
|
||||
@ -262,7 +263,7 @@ export const AmRoutesExpandedForm = ({
|
||||
return repeatIntervalValidator(value, groupInterval);
|
||||
},
|
||||
})}
|
||||
aria-label="Repeat interval value"
|
||||
aria-label={routeTimingsFields.repeatInterval.ariaLabel}
|
||||
className={formStyles.promDurationInput}
|
||||
/>
|
||||
</Field>
|
||||
|
@ -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',
|
||||
},
|
||||
};
|
@ -25,34 +25,14 @@ enum RoutingOptions {
|
||||
}
|
||||
|
||||
export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||
const { watch, setValue } = useFormContext<RuleFormValues>();
|
||||
const { watch } = useFormContext<RuleFormValues>();
|
||||
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) => {
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<RuleEditorSectionBody />
|
||||
{shouldAllowSimplifiedRouting ? ( // when simplified routing is enabled and is grafana rule
|
||||
<ManualAndAutomaticRouting alertUid={alertUid} />
|
||||
) : // when simplified routing is not enabled, render the notification preview as we did before
|
||||
shouldRenderpreview ? (
|
||||
<AutomaticRooting alertUid={alertUid} />
|
||||
) : null}
|
||||
</RuleEditorSection>
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 && (
|
||||
<NotificationPreview
|
||||
alertQueries={queries}
|
||||
customLabels={labels}
|
||||
condition={condition}
|
||||
folder={folder}
|
||||
alertName={alertName}
|
||||
alertUid={alertUid}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<Stack direction="column">
|
||||
<RadioButtonGroup
|
||||
options={routingOptions}
|
||||
value={manualRouting ? RoutingOptions.ContactPoint : RoutingOptions.NotificationPolicy}
|
||||
onChange={onRoutingOptionChange}
|
||||
className={styles.routingOptions}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<RoutingOptionDescription manualRouting={manualRouting} />
|
||||
|
||||
{manualRouting ? (
|
||||
<div className={styles.simplifiedRouting}>
|
||||
<SimplifiedRouting />
|
||||
</div>
|
||||
) : (
|
||||
shouldRenderPreview && (
|
||||
<NotificationPreview
|
||||
alertQueries={queries}
|
||||
customLabels={labels}
|
||||
condition={condition}
|
||||
folder={folder}
|
||||
alertName={alertName}
|
||||
alertUid={alertUid}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<RuleFormValues>();
|
||||
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 (
|
||||
<Stack direction="column">
|
||||
<Stack direction="column">
|
||||
<RadioButtonGroup
|
||||
options={routingOptions}
|
||||
value={manualRouting ? RoutingOptions.ContactPoint : RoutingOptions.NotificationPolicy}
|
||||
onChange={onRoutingOptionChange}
|
||||
className={styles.routingOptions}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<RoutingOptionDescription manualRouting={manualRouting} />
|
||||
|
||||
{manualRouting ? <SimplifiedRouting /> : <AutomaticRooting alertUid={alertUid} />}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
interface AutomaticRootingProps {
|
||||
alertUid?: string;
|
||||
}
|
||||
|
||||
function AutomaticRooting({ alertUid }: AutomaticRootingProps) {
|
||||
const { watch } = useFormContext<RuleFormValues>();
|
||||
const [labels, queries, condition, folder, alertName] = watch([
|
||||
'labels',
|
||||
'queries',
|
||||
'condition',
|
||||
'folder',
|
||||
'name',
|
||||
'manualRouting',
|
||||
]);
|
||||
return (
|
||||
<NotificationPreview
|
||||
alertQueries={queries}
|
||||
customLabels={labels}
|
||||
condition={condition}
|
||||
folder={folder}
|
||||
alertName={alertName}
|
||||
alertUid={alertUid}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 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',
|
||||
|
@ -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 <Alert title="Failed to fetch contact points" severity="error" />;
|
||||
}
|
||||
if (isLoading) {
|
||||
return <LoadingPlaceholder text={'Loading...'} />;
|
||||
}
|
||||
return (
|
||||
<Stack direction="column">
|
||||
{shouldShowAM && (
|
||||
<Stack direction="row" alignItems="center">
|
||||
<div className={styles.firstAlertManagerLine}></div>
|
||||
<div className={styles.alertManagerName}>
|
||||
Alert manager:
|
||||
<img src={alertManager.imgUrl} alt="Alert manager logo" className={styles.img} />
|
||||
{alertManagerName}
|
||||
</div>
|
||||
<div className={styles.secondAlertManagerLine}></div>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<ContactPointSelector
|
||||
alertManager={alertManagerName}
|
||||
contactPoints={contactPoints}
|
||||
onSelectContactPoint={setSelectedContactPointWithMetadata}
|
||||
/>
|
||||
<LinkToContactPoints />
|
||||
</Stack>
|
||||
{selectedContactPointWithMetadata?.grafana_managed_receiver_configs && (
|
||||
<ContactPointDetails receivers={selectedContactPointWithMetadata.grafana_managed_receiver_configs} />
|
||||
)}
|
||||
<div className={styles.routingSection}>
|
||||
<CollapsableSection label="Muting, grouping and timings" isOpen={false} className={styles.collapsableSection}>
|
||||
<Stack direction="column" gap={1}>
|
||||
<MuteTimingFields alertManager={alertManagerName} />
|
||||
<RoutingSettings alertManager={alertManagerName} />
|
||||
</Stack>
|
||||
</CollapsableSection>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
function LinkToContactPoints() {
|
||||
const hrefToContactPoints = '/alerting/notifications';
|
||||
return (
|
||||
<Link target="_blank" href={createUrl(hrefToContactPoints)} rel="noopener" aria-label="View alert rule">
|
||||
<Stack direction="row" gap={1} alignItems="center" justifyContent="center">
|
||||
<Text color="secondary">To browse contact points and create new ones go to</Text>
|
||||
<Text color="link">Contact points</Text>
|
||||
<Icon name={'external-link-alt'} size="sm" color="link" />
|
||||
</Stack>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
}),
|
||||
});
|
@ -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<AnyAction>;
|
||||
}
|
||||
export function ContactPointSelector({ selectedReceiver, alertManager, dispatch }: ContactPointSelectorProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const onChange = (value: SelectableValue<string>) => {
|
||||
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 = <ContactPointReceiverSummary receivers={integrations ?? []} />;
|
||||
|
||||
return { label: receiver.name, value: receiver.name, description };
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <Alert title="Failed to fetch contact points" severity="error" />;
|
||||
}
|
||||
if (isLoading) {
|
||||
return <LoadingPlaceholder text={'Loading...'} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<Field label="Contact point">
|
||||
<div className={styles.contactPointsSelector}>
|
||||
<Select
|
||||
aria-label="Contact point"
|
||||
onChange={onChange}
|
||||
// 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}
|
||||
value={selectedReceiver}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
contactPointsSelector: css({
|
||||
marginTop: theme.spacing(1),
|
||||
}),
|
||||
});
|
@ -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<AMContactPoint[]>([], (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<RuleFormValues>();
|
||||
const styles = useStyles2(getStyles);
|
||||
const { getValues } = useFormContext<RuleFormValues>();
|
||||
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 (
|
||||
<div key={index}>
|
||||
<Stack direction="column">
|
||||
{shouldShowAM && (
|
||||
<Stack direction="row" alignItems="center">
|
||||
<div className={styles.firstAlertManagerLine}></div>
|
||||
<div className={styles.alertManagerName}>
|
||||
{' '}
|
||||
Alert manager:
|
||||
<img
|
||||
src={alertManagerContactPoint.alertManager.imgUrl}
|
||||
alt="Alert manager logo"
|
||||
className={styles.img}
|
||||
/>
|
||||
{alertManagerContactPoint.alertManager.name}
|
||||
</div>
|
||||
<div className={styles.secondAlertManagerLine}></div>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<AlertmanagerProvider
|
||||
accessType={'notification'}
|
||||
alertmanagerSourceName={alertManagerContactPoint.alertManager.name}
|
||||
>
|
||||
<ContactPointSelector
|
||||
selectedReceiver={alertManagerContactPoint.selectedContactPoint}
|
||||
dispatch={dispatch}
|
||||
alertManager={alertManagerContactPoint.alertManager}
|
||||
/>
|
||||
</AlertmanagerProvider>
|
||||
<LinkToContactPoints />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
<AlertmanagerProvider
|
||||
accessType={'notification'}
|
||||
alertmanagerSourceName={alertManagerContactPoint.alertManager.name}
|
||||
key={alertManagerContactPoint.alertManager.name + index}
|
||||
>
|
||||
<AlertManagerManualRouting alertManager={alertManagerContactPoint.alertManager} />
|
||||
</AlertmanagerProvider>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function LinkToContactPoints() {
|
||||
const hrefToContactPoints = '/alerting/notifications';
|
||||
return (
|
||||
<Link target="_blank" href={createUrl(hrefToContactPoints)} rel="noopener" aria-label="View alert rule">
|
||||
<Stack direction="row" gap={1} alignItems="center" justifyContent="center">
|
||||
<Text color="secondary">To browse contact points and create new ones go to</Text>
|
||||
<Text color="link">Contact points</Text>
|
||||
<Icon name={'external-link-alt'} size="sm" color="link" />
|
||||
</Stack>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
}),
|
||||
});
|
||||
|
@ -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 (
|
||||
<Stack direction="column" gap={0}>
|
||||
<div>
|
||||
{receivers.map((receiver, index) => {
|
||||
const metadata = receiver[RECEIVER_META_KEY];
|
||||
const pluginMetadata = receiver[RECEIVER_PLUGIN_META_KEY];
|
||||
const key = metadata.name + index;
|
||||
return (
|
||||
<ContactPointReceiverTitleRow
|
||||
key={key}
|
||||
name={metadata.name}
|
||||
type={receiver.type}
|
||||
description={getReceiverDescription(receiver)}
|
||||
pluginMetadata={pluginMetadata}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
@ -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<RuleFormValues>();
|
||||
|
||||
const options = contactPoints.map((receiver) => {
|
||||
const integrations = receiver?.grafana_managed_receiver_configs;
|
||||
const description = <ContactPointReceiverSummary receivers={integrations ?? []} />;
|
||||
|
||||
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 (
|
||||
<Stack direction="column">
|
||||
<Field
|
||||
label="Contact point"
|
||||
{...register(`contactPoints.${alertManager}.selectedContactPoint`, { required: true })}
|
||||
invalid={!!errors.contactPoints?.[alertManager]?.selectedContactPoint}
|
||||
>
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => (
|
||||
<>
|
||||
<div className={styles.contactPointsSelector}>
|
||||
<Select
|
||||
{...field}
|
||||
defaultValue={selectedContactPointSelectableValue}
|
||||
aria-label="Contact point"
|
||||
onChange={(value: SelectableValue<ContactPointWithMetadata>, _: 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}
|
||||
/>
|
||||
</div>
|
||||
{error && <FieldValidationMessage>{'Contact point is required.'}</FieldValidationMessage>}
|
||||
</>
|
||||
)}
|
||||
control={control}
|
||||
name={`contactPoints.${alertManager}.selectedContactPoint`}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
contactPointsSelector: css({
|
||||
marginTop: theme.spacing(1),
|
||||
}),
|
||||
});
|
@ -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<RuleFormValues>();
|
||||
|
||||
const muteTimingOptions = useMuteTimingOptions();
|
||||
return (
|
||||
<Field
|
||||
label="Mute timings"
|
||||
data-testid="am-mute-timing-select"
|
||||
description="Add mute timing to policy"
|
||||
invalid={!!errors.contactPoints?.[alertManager]?.muteTimeIntervals}
|
||||
>
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<MultiSelect
|
||||
aria-label="Mute timings"
|
||||
{...field}
|
||||
className={styles.input}
|
||||
onChange={(value) => onChange(mapMultiSelectValueToStrings(value))}
|
||||
options={muteTimingOptions}
|
||||
/>
|
||||
)}
|
||||
control={control}
|
||||
name={`contactPoints.${alertManager}.muteTimeIntervals`}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
}
|
@ -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<RuleFormValues>();
|
||||
const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues([]));
|
||||
const { groupBy, groupIntervalValue, groupWaitValue, repeatIntervalValue } = useGetDefaultsForRoutingSettings();
|
||||
const overrideGrouping = watch(`contactPoints.${alertManager}.overrideGrouping`);
|
||||
const overrideTimings = watch(`contactPoints.${alertManager}.overrideTimings`);
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<Stack direction="row" gap={1} alignItems="center" justifyContent="space-between">
|
||||
<Field label="Override grouping">
|
||||
<Switch id="override-grouping-toggle" {...register(`contactPoints.${alertManager}.overrideGrouping`)} />
|
||||
</Field>
|
||||
{!overrideGrouping && (
|
||||
<Text variant="body" color="secondary">
|
||||
Grouping: <strong>{groupBy.join(', ')}</strong>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
{overrideGrouping && (
|
||||
<Field
|
||||
label="Group by"
|
||||
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}
|
||||
>
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => (
|
||||
<>
|
||||
<MultiSelect
|
||||
aria-label="Group by"
|
||||
{...field}
|
||||
allowCustomValue
|
||||
className={formStyles.input}
|
||||
onCreateOption={(opt: string) => {
|
||||
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 && <FieldValidationMessage>{'At least one group by option is required'}</FieldValidationMessage>}
|
||||
</>
|
||||
)}
|
||||
name={`contactPoints.${alertManager}.groupBy`}
|
||||
control={control}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<Stack direction="row" gap={1} alignItems="center" justifyContent="space-between">
|
||||
<Field label="Override timings">
|
||||
<Switch id="override-timings-toggle" {...register(`contactPoints.${alertManager}.overrideTimings`)} />
|
||||
</Field>
|
||||
{!overrideTimings && (
|
||||
<Text variant="body" color="secondary">
|
||||
Group wait: <strong>{groupWaitValue}, </strong>
|
||||
Group interval: <strong>{groupIntervalValue}, </strong>
|
||||
Repeat interval: <strong>{repeatIntervalValue}</strong>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
{overrideTimings && <RouteTimings alertManager={alertManager} />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
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]);
|
||||
}
|
@ -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<RuleFormValues>();
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
label={routeTimingsFields.groupWait.label}
|
||||
description={routeTimingsFields.groupWait.description}
|
||||
invalid={!!errors.contactPoints?.[alertManager]?.groupWaitValue}
|
||||
error={errors.contactPoints?.[alertManager]?.groupWaitValue?.message}
|
||||
>
|
||||
<PromDurationInput
|
||||
{...register(`contactPoints.${alertManager}.groupWaitValue`, { validate: promDurationValidator })}
|
||||
aria-label={routeTimingsFields.groupWait.ariaLabel}
|
||||
className={formStyles.promDurationInput}
|
||||
placeholder={TIMING_OPTIONS_DEFAULTS.group_wait}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={routeTimingsFields.groupInterval.label}
|
||||
description={routeTimingsFields.groupInterval.description}
|
||||
invalid={!!errors.contactPoints?.[alertManager]?.groupIntervalValue}
|
||||
error={errors.contactPoints?.[alertManager]?.groupIntervalValue?.message}
|
||||
>
|
||||
<PromDurationInput
|
||||
{...register(`contactPoints.${alertManager}.groupIntervalValue`, {
|
||||
validate: promDurationValidator,
|
||||
})}
|
||||
aria-label={routeTimingsFields.groupInterval.ariaLabel}
|
||||
className={formStyles.promDurationInput}
|
||||
placeholder={TIMING_OPTIONS_DEFAULTS.group_interval}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={routeTimingsFields.repeatInterval.label}
|
||||
description={routeTimingsFields.repeatInterval.description}
|
||||
invalid={!!errors.contactPoints?.[alertManager]?.repeatIntervalValue}
|
||||
error={errors.contactPoints?.[alertManager]?.repeatIntervalValue?.message}
|
||||
>
|
||||
<PromDurationInput
|
||||
{...register(`contactPoints.${alertManager}.repeatIntervalValue`, {
|
||||
validate: (value: string) => {
|
||||
const groupInterval = getValues(`contactPoints.${alertManager}.repeatIntervalValue`);
|
||||
return repeatIntervalValidator(value, groupInterval);
|
||||
},
|
||||
})}
|
||||
aria-label={routeTimingsFields.repeatInterval.ariaLabel}
|
||||
className={formStyles.promDurationInput}
|
||||
placeholder={TIMING_OPTIONS_DEFAULTS.repeat_interval}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user