Unified Alerting: UI explains "match all" case in Notification Policies. (#47574) (#47603)

This change makes it explicit in the UI that a notification policy with no matching labels matches all alerts that it processes. There are visual changes in both the Notification Policy editor, and in the Notification Policy table where matching columns are shown.

It's valid to have a notification policy with no label matchers attached. Such a policy matches all alerts sent to it. It's a common stumbling block for users. Where are all my alerts going?

Co-authored-by: gillesdemey <gilles.de.mey@gmail.com>
(cherry picked from commit 51c98b182d)

Co-authored-by: Joe Blubaugh <joe.blubaugh@grafana.com>
This commit is contained in:
Grot (@grafanabot)
2022-04-12 13:10:21 +02:00
committed by GitHub
co-authored by Joe Blubaugh
parent b477c7eba9
commit aa412e9678
2 changed files with 90 additions and 67 deletions
@@ -14,6 +14,8 @@ import {
Select,
Switch,
useStyles2,
Badge,
VerticalGroup,
} from '@grafana/ui';
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
import {
@@ -56,73 +58,85 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
<FieldArray name="object_matchers" control={control}>
{({ fields, append, remove }) => (
<>
<div>Matching labels</div>
<div className={styles.matchersContainer}>
{fields.map((field, index) => {
const localPath = `object_matchers[${index}]`;
return (
<HorizontalGroup key={field.id} align="flex-start">
<Field
label="Label"
invalid={!!errors.object_matchers?.[index]?.name}
error={errors.object_matchers?.[index]?.name?.message}
>
<Input
{...register(`${localPath}.name`, { required: 'Field is required' })}
defaultValue={field.name}
placeholder="label"
/>
</Field>
<Field label={'Operator'}>
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
className={styles.matchersOperator}
onChange={(value) => onChange(value?.value)}
options={matcherFieldOptions}
aria-label="Operator"
menuShouldPortal
<VerticalGroup justify="flex-start" spacing="md">
<div>Matching labels</div>
{fields.length === 0 && (
<Badge
color="orange"
className={styles.noMatchersWarning}
icon="exclamation-triangle"
text="If no matchers are specified, this notification policy will handle all alert instances."
/>
)}
{fields.length > 0 && (
<div className={styles.matchersContainer}>
{fields.map((field, index) => {
const localPath = `object_matchers[${index}]`;
return (
<HorizontalGroup key={field.id} align="flex-start">
<Field
label="Label"
invalid={!!errors.object_matchers?.[index]?.name}
error={errors.object_matchers?.[index]?.name?.message}
>
<Input
{...register(`${localPath}.name`, { required: 'Field is required' })}
defaultValue={field.name}
placeholder="label"
/>
)}
defaultValue={field.operator}
control={control}
name={`${localPath}.operator` as const}
rules={{ required: { value: true, message: 'Required.' } }}
/>
</Field>
<Field
label="Value"
invalid={!!errors.object_matchers?.[index]?.value}
error={errors.object_matchers?.[index]?.value?.message}
>
<Input
{...register(`${localPath}.value`, { required: 'Field is required' })}
defaultValue={field.value}
placeholder="value"
/>
</Field>
<IconButton
className={styles.removeButton}
tooltip="Remove matcher"
name={'trash-alt'}
onClick={() => remove(index)}
>
Remove
</IconButton>
</HorizontalGroup>
);
})}
</div>
<Button
className={styles.addMatcherBtn}
icon="plus"
onClick={() => append(emptyArrayFieldMatcher)}
variant="secondary"
type="button"
>
Add matcher
</Button>
</Field>
<Field label={'Operator'}>
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
className={styles.matchersOperator}
onChange={(value) => onChange(value?.value)}
options={matcherFieldOptions}
aria-label="Operator"
menuShouldPortal
/>
)}
defaultValue={field.operator}
control={control}
name={`${localPath}.operator` as const}
rules={{ required: { value: true, message: 'Required.' } }}
/>
</Field>
<Field
label="Value"
invalid={!!errors.object_matchers?.[index]?.value}
error={errors.object_matchers?.[index]?.value?.message}
>
<Input
{...register(`${localPath}.value`, { required: 'Field is required' })}
defaultValue={field.value}
placeholder="value"
/>
</Field>
<IconButton
className={styles.removeButton}
tooltip="Remove matcher"
name={'trash-alt'}
onClick={() => remove(index)}
>
Remove
</IconButton>
</HorizontalGroup>
);
})}
</div>
)}
<Button
className={styles.addMatcherBtn}
icon="plus"
onClick={() => append(emptyArrayFieldMatcher)}
variant="secondary"
type="button"
>
Add matcher
</Button>
</VerticalGroup>
</>
)}
</FieldArray>
@@ -376,5 +390,8 @@ const getStyles = (theme: GrafanaTheme2) => {
margin-left: ${theme.spacing(1.5)};
}
`,
noMatchersWarning: css`
padding: ${theme.spacing(1)} ${theme.spacing(2)};
`,
};
};
@@ -90,7 +90,13 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
id: 'matchingCriteria',
label: 'Matching labels',
// eslint-disable-next-line react/display-name
renderCell: (item) => <Matchers matchers={item.data.object_matchers.map(matcherFieldToMatcher)} />,
renderCell: (item) => {
return item.data.object_matchers.length ? (
<Matchers matchers={item.data.object_matchers.map(matcherFieldToMatcher)} />
) : (
<span>Matches all alert instances</span>
);
},
size: 10,
},
{