refactoring
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
ComboboxOption,
|
||||
Field,
|
||||
Input,
|
||||
Label,
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
@@ -23,13 +24,16 @@ import { RuleFormValues } from '../types/rule-form';
|
||||
|
||||
import { NeedHelpInfoForNotificationPolicy } from './rule-editor/NotificationsStep';
|
||||
|
||||
// Centralized form path for selected contact point
|
||||
const CONTACT_POINT_PATH = 'contactPoints.grafana.selectedContactPoint' as const;
|
||||
|
||||
export function RuleNotificationSection() {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { watch, setValue } = useFormContext<RuleFormValues>();
|
||||
const manualRouting = watch('manualRouting');
|
||||
const useNotificationPolicy = !manualRouting;
|
||||
const selectedContactPoint = watch('contactPoints.grafana.selectedContactPoint');
|
||||
const selectedContactPoint = watch(CONTACT_POINT_PATH);
|
||||
// Fetch contact points from Alerting API v0alpha1
|
||||
const { currentData, status, refetch } = notificationsAPIv0alpha1.endpoints.listReceiver.useQuery({});
|
||||
const options = useMemo<Array<ComboboxOption<string>>>(
|
||||
@@ -41,6 +45,8 @@ export function RuleNotificationSection() {
|
||||
[currentData]
|
||||
);
|
||||
|
||||
const recipientLabelId = 'recipient-label';
|
||||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeaderRow}>
|
||||
@@ -54,100 +60,107 @@ export function RuleNotificationSection() {
|
||||
|
||||
<div className={styles.contentIndented}>
|
||||
<Stack direction="column" gap={2}>
|
||||
<Field
|
||||
label={
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" gap={1}>
|
||||
<span>{t('alerting.simplified.notification.recipient.label', 'Recipient')}</span>
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{t('alerting.simplified.notification.manual-routing.label', 'Manual routing')}
|
||||
</Text>
|
||||
<Switch
|
||||
value={manualRouting}
|
||||
onChange={(e) => {
|
||||
const next = e.currentTarget.checked;
|
||||
setValue('manualRouting', next, { shouldDirty: true });
|
||||
setValue('editorSettings.simplifiedNotificationEditor', next, { shouldDirty: true });
|
||||
}}
|
||||
aria-label={t('alerting.simplified.notification.manual-routing.aria', 'Toggle manual routing')}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
noMargin
|
||||
>
|
||||
<Stack direction="column" gap={1}>
|
||||
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" gap={1}>
|
||||
<Label
|
||||
htmlFor={recipientLabelId}
|
||||
description={
|
||||
useNotificationPolicy ? (
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
<Trans i18nKey="alerting.simplified.notification.policy-selected">
|
||||
Notifications for firing alerts are routed to contact points based on matching labels and the
|
||||
notification policy tree.
|
||||
</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
<Trans i18nKey="alerting.simplified.notification.contact-point-selected">
|
||||
Notifications for firing alerts are routed to a selected contact point.
|
||||
</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
>
|
||||
<span id={recipientLabelId}>{t('alerting.simplified.notification.recipient.label', 'Recipient')}</span>
|
||||
</Label>
|
||||
<div className={styles.manualRoutingInline}>
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{t('alerting.simplified.notification.manual-routing.label', 'Manual routing')}
|
||||
</Text>
|
||||
<Switch
|
||||
value={manualRouting}
|
||||
onChange={(e) => {
|
||||
const next = e.currentTarget.checked;
|
||||
setValue('manualRouting', next, { shouldDirty: true, shouldValidate: true });
|
||||
setValue('editorSettings.simplifiedNotificationEditor', next, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
}}
|
||||
aria-labelledby={recipientLabelId}
|
||||
aria-label={t('alerting.simplified.notification.manual-routing.aria', 'Toggle manual routing')}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
{useNotificationPolicy ? (
|
||||
<div className={styles.contentTopSpacer}>
|
||||
<Stack direction="column" gap={1} alignItems="start">
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
<Trans i18nKey="alerting.simplified.notification.policy-selected">
|
||||
Notifications for firing alerts are routed to contact points based on matching labels and the
|
||||
notification policy tree.
|
||||
</Trans>
|
||||
</Text>
|
||||
<NeedHelpInfoForNotificationPolicy />
|
||||
</Stack>
|
||||
<NeedHelpInfoForNotificationPolicy />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.contentTopSpacer}>
|
||||
<Stack direction="column" gap={1}>
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
<Trans i18nKey="alerting.simplified.notification.contact-point-selected">
|
||||
Notifications for firing alerts are routed to a selected contact point.
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<Combobox<ComboboxOption<string>['value']>
|
||||
options={options}
|
||||
value={
|
||||
selectedContactPoint ? (options.find((o) => o.value === selectedContactPoint) ?? null) : null
|
||||
}
|
||||
onChange={(opt) =>
|
||||
setValue(CONTACT_POINT_PATH, opt?.value ?? '', {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
width={30}
|
||||
placeholder={t(
|
||||
'alerting.simplified.notification.select-contact-point',
|
||||
'Select a contact point...'
|
||||
)}
|
||||
isClearable
|
||||
data-testid="contact-point"
|
||||
loading={status === 'pending'}
|
||||
/>
|
||||
<Button
|
||||
icon="sync"
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
size="sm"
|
||||
aria-label={t('alerting.common.refresh', 'Refresh')}
|
||||
onClick={() => {
|
||||
if (refetch) {
|
||||
refetch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextLink
|
||||
external
|
||||
href={'/alerting/notifications'}
|
||||
aria-label={t(
|
||||
'alerting.link-to-contact-points.aria-label-view-or-create-contact-points',
|
||||
'View or create contact points'
|
||||
)}
|
||||
>
|
||||
<Trans i18nKey="alerting.link-to-contact-points.view-or-create-contact-points">
|
||||
View or create contact points
|
||||
</Trans>
|
||||
</Text>
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<Combobox<ComboboxOption<string>['value']>
|
||||
options={options}
|
||||
value={
|
||||
selectedContactPoint ? (options.find((o) => o.value === selectedContactPoint) ?? null) : null
|
||||
}
|
||||
onChange={(opt) =>
|
||||
setValue('contactPoints.grafana.selectedContactPoint', opt?.value ?? '', {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
width={30}
|
||||
placeholder={t(
|
||||
'alerting.simplified.notification.select-contact-point',
|
||||
'Select a contact point...'
|
||||
)}
|
||||
isClearable
|
||||
data-testid="contact-point"
|
||||
loading={status === 'pending'}
|
||||
/>
|
||||
<Button
|
||||
icon="sync"
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
size="sm"
|
||||
aria-label={t('alerting.common.refresh', 'Refresh')}
|
||||
onClick={() => {
|
||||
if (refetch) {
|
||||
refetch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextLink
|
||||
external
|
||||
href={'/alerting/notifications'}
|
||||
aria-label={t(
|
||||
'alerting.link-to-contact-points.aria-label-view-or-create-contact-points',
|
||||
'View or create contact points'
|
||||
)}
|
||||
>
|
||||
<Trans i18nKey="alerting.link-to-contact-points.view-or-create-contact-points">
|
||||
View or create contact points
|
||||
</Trans>
|
||||
</TextLink>
|
||||
</Stack>
|
||||
</TextLink>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</Stack>
|
||||
|
||||
<Field label={t('alerting.simplified.notification.summary.label', 'Summary (optional)')} noMargin>
|
||||
<TextArea
|
||||
id="summary-text-area"
|
||||
placeholder={t(
|
||||
'alerting.simplified.notification.summary.placeholder',
|
||||
'Enter a summary of what happened and why…'
|
||||
@@ -157,6 +170,7 @@ export function RuleNotificationSection() {
|
||||
|
||||
<Field label={t('alerting.simplified.notification.description.label', 'Description (optional)')} noMargin>
|
||||
<TextArea
|
||||
id="description-text-area"
|
||||
placeholder={t(
|
||||
'alerting.simplified.notification.description.placeholder',
|
||||
'Enter a description of what the alert rule does…'
|
||||
@@ -166,6 +180,7 @@ export function RuleNotificationSection() {
|
||||
|
||||
<Field label={t('alerting.simplified.notification.runbook-url.label', 'Runbook URL (optional)')} noMargin>
|
||||
<Input
|
||||
id="runbook-url-input"
|
||||
placeholder={t(
|
||||
'alerting.simplified.notification.runbook-url.placeholder',
|
||||
'Enter the webpage where you keep your runbook for the alert…'
|
||||
@@ -206,5 +221,11 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
}),
|
||||
contentIndented: css({ marginLeft: `calc(20px + ${theme.spacing(1)})` }),
|
||||
contentTopSpacer: css({ marginTop: theme.spacing(0.5) }),
|
||||
manualRoutingInline: css({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(0.5),
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
+10
-6
@@ -1,6 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useId, useMemo, useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
@@ -16,8 +15,11 @@ import { ProvisioningBadge } from '../Provisioning';
|
||||
|
||||
import { EvaluationGroupCreationModal } from './GrafanaEvaluationBehavior';
|
||||
|
||||
export type GroupOption = SelectableValue<string> & { isProvisioned?: boolean };
|
||||
|
||||
export function EvaluationGroupFieldRow({ enableProvisionedGroups }: { enableProvisionedGroups: boolean }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const groupInputId = useId();
|
||||
|
||||
const {
|
||||
watch,
|
||||
@@ -31,7 +33,6 @@ export function EvaluationGroupFieldRow({ enableProvisionedGroups }: { enablePro
|
||||
const { currentData: rulerNamespace, isLoading: loadingGroups } = useFetchGroupsForFolder(folder?.uid ?? '');
|
||||
|
||||
const collator = useMemo(() => new Intl.Collator(), []);
|
||||
type GroupOption = SelectableValue<string> & { isProvisioned?: boolean };
|
||||
const groupOptions = useMemo<GroupOption[]>(() => {
|
||||
if (!rulerNamespace) {
|
||||
return [];
|
||||
@@ -71,7 +72,7 @@ export function EvaluationGroupFieldRow({ enableProvisionedGroups }: { enablePro
|
||||
|
||||
return (
|
||||
<Stack alignItems="center">
|
||||
<div style={{ width: 420 }}>
|
||||
<div className={styles.formContainer}>
|
||||
<Field
|
||||
noMargin
|
||||
label={label}
|
||||
@@ -85,8 +86,7 @@ export function EvaluationGroupFieldRow({ enableProvisionedGroups }: { enablePro
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<Select
|
||||
disabled={!folder?.uid || loadingGroups}
|
||||
inputId="group"
|
||||
key={uniqueId()}
|
||||
inputId={groupInputId}
|
||||
{...field}
|
||||
onChange={(group) => {
|
||||
field.onChange(group.label ?? '');
|
||||
@@ -176,6 +176,10 @@ export function EvaluationGroupFieldRow({ enableProvisionedGroups }: { enablePro
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
formContainer: css({
|
||||
width: '100%',
|
||||
maxWidth: theme.breakpoints.values.sm,
|
||||
}),
|
||||
formInput: css({
|
||||
flexGrow: 1,
|
||||
}),
|
||||
|
||||
@@ -52,6 +52,35 @@ describe('formValuesToRulerGrafanaRuleDTO', () => {
|
||||
expect(formValuesToRulerGrafanaRuleDTO(formValues)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('sets notification_settings.receiver only when manualRouting is true', () => {
|
||||
const base: RuleFormValues = {
|
||||
...getDefaultFormValues(),
|
||||
type: RuleFormType.grafana,
|
||||
condition: 'A',
|
||||
contactPoints: {
|
||||
grafana: {
|
||||
selectedContactPoint: 'team-receiver',
|
||||
muteTimeIntervals: [],
|
||||
activeTimeIntervals: [],
|
||||
overrideGrouping: false,
|
||||
overrideTimings: false,
|
||||
groupBy: [],
|
||||
groupWaitValue: '',
|
||||
groupIntervalValue: '',
|
||||
repeatIntervalValue: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// manualRouting false → no notification_settings
|
||||
const dtoNoManual = formValuesToRulerGrafanaRuleDTO({ ...base, manualRouting: false });
|
||||
expect(dtoNoManual.grafana_alert.notification_settings).toBeUndefined();
|
||||
|
||||
// manualRouting true → notification_settings.receiver present
|
||||
const dtoManual = formValuesToRulerGrafanaRuleDTO({ ...base, manualRouting: true });
|
||||
expect(dtoManual.grafana_alert.notification_settings?.receiver).toBe('team-receiver');
|
||||
});
|
||||
|
||||
it('should not save both instant and range type queries', () => {
|
||||
const defaultValues = getDefaultFormValues();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user