mirror of
https://github.com/grafana/grafana.git
synced 2025-01-23 15:03:41 -06:00
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:
parent
9008ebd27a
commit
6579872122
@ -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};
|
||||
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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[];
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user