Alerting: update matchers field operators to Select (#37809)

* Alerting: update matchers field operators to Select

* Add matcher field to routes

* fix default values

* min-width for matcher operator

* dry up matcher field options

* change MatcherField name to MatcherValue
This commit is contained in:
Nathan Rodman 2021-08-17 14:48:39 -07:00 committed by GitHub
parent 9008ebd27a
commit 6579872122
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 40 deletions

View File

@ -3,7 +3,6 @@ import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import {
Button,
Checkbox,
Field,
FieldArray,
Form,
@ -27,6 +26,7 @@ import {
} from '../../utils/amroutes';
import { timeOptions } from '../../utils/time';
import { getFormStyles } from './formStyles';
import { matcherFieldOptions } from '../../utils/alertmanager';
export interface AmRoutesExpandedFormProps {
onCancel: () => void;
@ -71,6 +71,22 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
placeholder="label"
/>
</Field>
<Field label={'Operator'}>
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
className={styles.matchersOperator}
onChange={(value) => onChange(value?.value)}
options={matcherFieldOptions}
/>
)}
defaultValue={field.operator}
control={control}
name={`${localPath}.operator` as const}
rules={{ required: { value: true, message: 'Required.' } }}
/>
</Field>
<Field
label="Value"
invalid={!!errors.matchers?.[index]?.value}
@ -82,12 +98,6 @@ export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel,
placeholder="value"
/>
</Field>
<Field className={styles.matcherRegexField} label="Regex">
<Checkbox {...register(`${localPath}.isRegex`)} defaultChecked={field.isRegex} />
</Field>
<Field className={styles.matcherRegexField} label="Equal">
<Checkbox {...register(`${localPath}.isEqual`)} defaultChecked={field.isEqual} />
</Field>
<IconButton
className={styles.removeButton}
tooltip="Remove matcher"
@ -299,8 +309,8 @@ const getStyles = (theme: GrafanaTheme2) => {
padding: ${theme.spacing(1, 4.6, 1, 1.5)};
width: fit-content;
`,
matcherRegexField: css`
margin-left: ${theme.spacing(6)};
matchersOperator: css`
min-width: 140px;
`,
nestedPolicies: css`
margin-top: ${commonSpacing};

View File

@ -6,6 +6,7 @@ import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '..
import { AmRoutesExpandedForm } from './AmRoutesExpandedForm';
import { AmRoutesExpandedRead } from './AmRoutesExpandedRead';
import { Matchers } from '../silences/Matchers';
import { matcherFieldToMatcher } from '../../utils/alertmanager';
export interface AmRoutesTableProps {
isAddMode: boolean;
@ -32,7 +33,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({ isAddMode, onCancelAdd,
id: 'matchingCriteria',
label: 'Matching labels',
// eslint-disable-next-line react/display-name
renderCell: (item) => <Matchers matchers={item.data.matchers} />,
renderCell: (item) => <Matchers matchers={item.data.matchers.map(matcherFieldToMatcher)} />,
size: 10,
},
{

View File

@ -1,10 +1,11 @@
import React, { FC } from 'react';
import { Button, Field, Input, Checkbox, IconButton, useStyles2 } from '@grafana/ui';
import { Button, Field, Input, IconButton, InputControl, useStyles2, Select } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { css, cx } from '@emotion/css';
import { useFormContext, useFieldArray } from 'react-hook-form';
import { SilenceFormFields } from '../../types/silence-form';
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
import { matcherToOperator, matcherFieldToMatcher, matcherFieldOptions } from '../../utils/alertmanager';
interface Props {
className?: string;
@ -14,9 +15,11 @@ const MatchersField: FC<Props> = ({ className }) => {
const styles = useStyles2(getStyles);
const formApi = useFormContext<SilenceFormFields>();
const {
control,
register,
formState: { errors },
} = formApi;
const { fields: matchers = [], append, remove } = useFieldArray<SilenceFormFields>({ name: 'matchers' });
return (
@ -40,6 +43,26 @@ const MatchersField: FC<Props> = ({ className }) => {
placeholder="label"
/>
</Field>
<Field label={'Operator'}>
<InputControl
control={control}
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
className={styles.matcherOptions}
onChange={(value) => onChange(value?.value)}
options={matcherFieldOptions}
/>
)}
defaultValue={
matcherFieldOptions.find(
(field) => field.label === matcherToOperator(matcherFieldToMatcher(matcher))
) || matcherFieldOptions[0]
}
name={`matchers.${index}.operator` as const}
rules={{ required: { value: true, message: 'Required.' } }}
/>
</Field>
<Field
label="Value"
invalid={!!errors?.matchers?.[index]?.value}
@ -53,12 +76,6 @@ const MatchersField: FC<Props> = ({ className }) => {
placeholder="value"
/>
</Field>
<Field label="Regex">
<Checkbox {...register(`matchers.${index}.isRegex` as const)} defaultChecked={matcher.isRegex} />
</Field>
<Field label="Equal">
<Checkbox {...register(`matchers.${index}.isEqual` as const)} defaultChecked={matcher.isEqual} />
</Field>
{matchers.length > 1 && (
<IconButton
className={styles.removeButton}
@ -78,7 +95,7 @@ const MatchersField: FC<Props> = ({ className }) => {
icon="plus"
variant="secondary"
onClick={() => {
const newMatcher: Matcher = { name: '', value: '', isRegex: false, isEqual: true };
const newMatcher = { name: '', value: '', operator: MatcherOperator.equal };
append(newMatcher);
}}
>
@ -109,6 +126,9 @@ const getStyles = (theme: GrafanaTheme2) => {
margin-left: ${theme.spacing(1)};
margin-top: ${theme.spacing(2.5)};
`,
matcherOptions: css`
min-width: 140px;
`,
matchers: css`
max-width: 585px;
margin: ${theme.spacing(1)} 0;

View File

@ -1,4 +1,4 @@
import { Silence, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types';
import { MatcherOperator, Silence, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types';
import React, { FC, useMemo, useState } from 'react';
import { Button, Field, FieldSet, Input, LinkButton, TextArea, useStyles } from '@grafana/ui';
import {
@ -26,6 +26,7 @@ import { makeAMLink } from '../../utils/misc';
import { useCleanup } from 'app/core/hooks/useCleanup';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { parseQueryParamMatchers } from '../../utils/matchers';
import { matcherToMatcherField, matcherFieldToMatcher } from '../../utils/alertmanager';
interface Props {
silence?: Silence;
@ -40,7 +41,7 @@ const defaultsFromQuery = (queryParams: UrlQueryMap): Partial<SilenceFormFields>
if (typeof matchers === 'string') {
const formMatchers = parseQueryParamMatchers(matchers);
if (formMatchers.length) {
defaults.matchers = formMatchers;
defaults.matchers = formMatchers.map(matcherToMatcherField);
}
}
@ -69,7 +70,7 @@ const getDefaultFormValues = (queryParams: UrlQueryMap, silence?: Silence): Sile
createdBy: silence.createdBy,
duration: intervalToAbbreviatedDurationString(interval),
isRegex: false,
matchers: silence.matchers || [],
matchers: silence.matchers?.map(matcherToMatcherField) || [],
matcherName: '',
matcherValue: '',
timeZone: DefaultTimeZone,
@ -84,7 +85,7 @@ const getDefaultFormValues = (queryParams: UrlQueryMap, silence?: Silence): Sile
createdBy: config.bootData.user.name,
duration: '2h',
isRegex: false,
matchers: [{ name: '', value: '', isRegex: false, isEqual: true }],
matchers: [{ name: '', value: '', operator: MatcherOperator.equal }],
matcherName: '',
matcherValue: '',
timeZone: DefaultTimeZone,
@ -107,7 +108,8 @@ export const SilencesEditor: FC<Props> = ({ silence, alertManagerSourceName }) =
const { register, handleSubmit, formState, watch, setValue, clearErrors } = formAPI;
const onSubmit = (data: SilenceFormFields) => {
const { id, startsAt, endsAt, comment, createdBy, matchers } = data;
const { id, startsAt, endsAt, comment, createdBy, matchers: matchersFields } = data;
const matchers = matchersFields.map(matcherFieldToMatcher);
const payload = pickBy(
{
id,

View File

@ -1,8 +1,8 @@
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { MatcherFieldValue } from './silence-form';
export interface FormAmRoute {
id: string;
matchers: Matcher[];
matchers: MatcherFieldValue[];
continue: boolean;
receiver: string;
groupBy: string[];

View File

@ -1,5 +1,11 @@
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { TimeZone } from '@grafana/data';
import { MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
export type MatcherFieldValue = {
name: string;
value: string;
operator: MatcherOperator;
};
export type SilenceFormFields = {
id: string;
@ -8,7 +14,7 @@ export type SilenceFormFields = {
timeZone: TimeZone;
duration: string;
comment: string;
matchers: Matcher[];
matchers: MatcherFieldValue[];
createdBy: string;
matcherName: string;
matcherValue: string;

View File

@ -1,5 +1,7 @@
import { AlertManagerCortexConfig, MatcherOperator, Route, Matcher } from 'app/plugins/datasource/alertmanager/types';
import { Labels } from 'app/types/unified-alerting-dto';
import { MatcherFieldValue } from '../types/silence-form';
import { SelectableValue } from '@grafana/data';
export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
// add default receiver if it does not exist
@ -44,6 +46,42 @@ export function matcherToOperator(matcher: Matcher): MatcherOperator {
}
}
export function matcherOperatorToValue(operator: MatcherOperator) {
switch (operator) {
case MatcherOperator.equal:
return { isEqual: true, isRegex: false };
case MatcherOperator.notEqual:
return { isEqual: false, isRegex: false };
case MatcherOperator.regex:
return { isEqual: true, isRegex: true };
case MatcherOperator.notRegex:
return { isEqual: false, isRegex: true };
}
}
export function matcherToMatcherField(matcher: Matcher): MatcherFieldValue {
return {
name: matcher.name,
value: matcher.value,
operator: matcherToOperator(matcher),
};
}
export function matcherFieldToMatcher(field: MatcherFieldValue): Matcher {
return {
name: field.name,
value: field.value,
...matcherOperatorToValue(field.operator),
};
}
export const matcherFieldOptions: SelectableValue[] = [
{ label: MatcherOperator.equal, description: 'Equals', value: MatcherOperator.equal },
{ label: MatcherOperator.notEqual, description: 'Does not equal', value: MatcherOperator.notEqual },
{ label: MatcherOperator.regex, description: 'Matches regex', value: MatcherOperator.regex },
{ label: MatcherOperator.notRegex, description: 'Does not match regex', value: MatcherOperator.notRegex },
];
const matcherOperators = [
MatcherOperator.regex,
MatcherOperator.notRegex,

View File

@ -1,25 +1,28 @@
import { SelectableValue } from '@grafana/data';
import { Validate } from 'react-hook-form';
import { Matcher, Route } from 'app/plugins/datasource/alertmanager/types';
import { MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types';
import { FormAmRoute } from '../types/amroutes';
import { parseInterval, timeOptions } from './time';
import { parseMatcher, stringifyMatcher } from './alertmanager';
import { matcherToMatcherField, matcherFieldToMatcher, parseMatcher, stringifyMatcher } from './alertmanager';
import { isUndefined, omitBy } from 'lodash';
import { MatcherFieldValue } from '../types/silence-form';
const defaultValueAndType: [string, string] = ['', timeOptions[0].value];
const matchersToArrayFieldMatchers = (matchers: Record<string, string> | undefined, isRegex: boolean): Matcher[] =>
Object.entries(matchers ?? {}).reduce<Matcher[]>(
const matchersToArrayFieldMatchers = (
matchers: Record<string, string> | undefined,
isRegex: boolean
): MatcherFieldValue[] =>
Object.entries(matchers ?? {}).reduce<MatcherFieldValue[]>(
(acc, [name, value]) => [
...acc,
{
name,
value,
isRegex: isRegex,
isEqual: true,
operator: isRegex ? MatcherOperator.regex : MatcherOperator.equal,
},
],
[] as Matcher[]
[] as MatcherFieldValue[]
);
const intervalToValueAndType = (strValue: string | undefined): [string, string] => {
@ -43,11 +46,10 @@ const selectableValueToString = (selectableValue: SelectableValue<string>): stri
const selectableValuesToStrings = (arr: Array<SelectableValue<string>> | undefined): string[] =>
(arr ?? []).map(selectableValueToString);
export const emptyArrayFieldMatcher: Matcher = {
export const emptyArrayFieldMatcher: MatcherFieldValue = {
name: '',
value: '',
isRegex: false,
isEqual: true,
operator: MatcherOperator.equal,
};
export const emptyRoute: FormAmRoute = {
@ -90,7 +92,7 @@ export const amRouteToFormAmRoute = (route: Route | undefined): [FormAmRoute, Re
{
id,
matchers: [
...(route.matchers?.map(parseMatcher) ?? []),
...(route.matchers?.map((matcher) => matcherToMatcherField(parseMatcher(matcher))) ?? []),
...matchersToArrayFieldMatchers(route.match, false),
...matchersToArrayFieldMatchers(route.match_re, true),
],
@ -115,7 +117,9 @@ export const formAmRouteToAmRoute = (formAmRoute: FormAmRoute, id2ExistingRoute:
...(existing ?? {}),
continue: formAmRoute.continue,
group_by: formAmRoute.groupBy,
matchers: formAmRoute.matchers.length ? formAmRoute.matchers.map(stringifyMatcher) : undefined,
matchers: formAmRoute.matchers.length
? formAmRoute.matchers.map((matcher) => stringifyMatcher(matcherFieldToMatcher(matcher)))
: undefined,
match: undefined,
match_re: undefined,
group_wait: formAmRoute.groupWaitValue