diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx index 9e0c66f91cd..89480432529 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx @@ -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 = ({ onCancel, placeholder="label" /> + + ( + 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.' } }} + /> + = ({ className }) => { placeholder="value" /> - - - - - - {matchers.length > 1 && ( = ({ 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; diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx index ea1b1f51a83..e5a46123ca1 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx @@ -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 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 = ({ 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, diff --git a/public/app/features/alerting/unified/types/amroutes.ts b/public/app/features/alerting/unified/types/amroutes.ts index d999e979848..5da7ac59f4c 100644 --- a/public/app/features/alerting/unified/types/amroutes.ts +++ b/public/app/features/alerting/unified/types/amroutes.ts @@ -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[]; diff --git a/public/app/features/alerting/unified/types/silence-form.ts b/public/app/features/alerting/unified/types/silence-form.ts index 0fcc8e22b67..7f99446efc0 100644 --- a/public/app/features/alerting/unified/types/silence-form.ts +++ b/public/app/features/alerting/unified/types/silence-form.ts @@ -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; diff --git a/public/app/features/alerting/unified/utils/alertmanager.ts b/public/app/features/alerting/unified/utils/alertmanager.ts index d67db6b130c..1ded7b86c94 100644 --- a/public/app/features/alerting/unified/utils/alertmanager.ts +++ b/public/app/features/alerting/unified/utils/alertmanager.ts @@ -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, diff --git a/public/app/features/alerting/unified/utils/amroutes.ts b/public/app/features/alerting/unified/utils/amroutes.ts index f2fd0c0b922..9ab34b9e28d 100644 --- a/public/app/features/alerting/unified/utils/amroutes.ts +++ b/public/app/features/alerting/unified/utils/amroutes.ts @@ -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 | undefined, isRegex: boolean): Matcher[] => - Object.entries(matchers ?? {}).reduce( +const matchersToArrayFieldMatchers = ( + matchers: Record | undefined, + isRegex: boolean +): MatcherFieldValue[] => + Object.entries(matchers ?? {}).reduce( (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): stri const selectableValuesToStrings = (arr: Array> | 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