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

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>
This commit is contained in:
Joe Blubaugh 2022-04-12 10:42:17 +08:00 committed by GitHub
parent 7a8437020d
commit 51c98b182d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 90 additions and 67 deletions

View File

@ -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)};
`,
};
};

View File

@ -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,
},
{