{tableItemAlerts.length > 0 ? (
diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
index e13bbbe0819..a8a5d70eeb6 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
@@ -1,13 +1,12 @@
-import { css, cx } from '@emotion/css';
-import { isEqual, pickBy } from 'lodash';
-import React, { useEffect, useMemo, useState } from 'react';
+import { css } from '@emotion/css';
+import { pickBy } from 'lodash';
+import React, { useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useDebounce } from 'react-use';
import {
addDurationToDate,
dateTime,
- DefaultTimeZone,
GrafanaTheme2,
intervalToAbbreviatedDurationString,
isValidDate,
@@ -22,109 +21,113 @@ import {
Input,
LinkButton,
LoadingPlaceholder,
+ Stack,
TextArea,
useStyles2,
} from '@grafana/ui';
-import { alertSilencesApi } from 'app/features/alerting/unified/api/alertSilencesApi';
+import { alertSilencesApi, SilenceCreatedResponse } from 'app/features/alerting/unified/api/alertSilencesApi';
+import { MATCHER_ALERT_RULE_UID } from 'app/features/alerting/unified/utils/constants';
import { getDatasourceAPIUid } from 'app/features/alerting/unified/utils/datasource';
-import { Matcher, MatcherOperator, Silence, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types';
+import { MatcherOperator, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types';
-import { useURLSearchParams } from '../../hooks/useURLSearchParams';
import { SilenceFormFields } from '../../types/silence-form';
-import { matcherFieldToMatcher, matcherToMatcherField } from '../../utils/alertmanager';
-import { parseQueryParamMatchers } from '../../utils/matchers';
+import { matcherFieldToMatcher } from '../../utils/alertmanager';
import { makeAMLink } from '../../utils/misc';
import MatchersField from './MatchersField';
import { SilencePeriod } from './SilencePeriod';
import { SilencedInstancesPreview } from './SilencedInstancesPreview';
+import { getDefaultSilenceFormValues, getFormFieldsForSilence } from './utils';
interface Props {
- silenceId?: string;
+ silenceId: string;
alertManagerSourceName: string;
}
-const defaultsFromQuery = (searchParams: URLSearchParams): Partial
=> {
- const defaults: Partial = {};
+/**
+ * Silences editor for editing an existing silence.
+ *
+ * Fetches silence details from API, based on `silenceId`
+ */
+export const ExistingSilenceEditor = ({ silenceId, alertManagerSourceName }: Props) => {
+ const {
+ data: silence,
+ isLoading: getSilenceIsLoading,
+ error: errorGettingExistingSilence,
+ } = alertSilencesApi.endpoints.getSilence.useQuery({
+ id: silenceId,
+ datasourceUid: getDatasourceAPIUid(alertManagerSourceName),
+ });
- const comment = searchParams.get('comment');
- const matchers = searchParams.getAll('matcher');
+ const ruleUid = silence?.matchers?.find((m) => m.name === MATCHER_ALERT_RULE_UID)?.value;
- const formMatchers = parseQueryParamMatchers(matchers);
- if (formMatchers.length) {
- defaults.matchers = formMatchers.map(matcherToMatcherField);
+ const defaultValues = useMemo(() => {
+ if (!silence) {
+ return;
+ }
+ const filteredMatchers = silence.matchers?.filter((m) => m.name !== MATCHER_ALERT_RULE_UID);
+ return getFormFieldsForSilence({ ...silence, matchers: filteredMatchers });
+ }, [silence]);
+
+ if (silenceId && getSilenceIsLoading) {
+ return ;
}
- if (comment) {
- defaults.comment = comment;
+ const existingSilenceNotFound =
+ isFetchError(errorGettingExistingSilence) && errorGettingExistingSilence.status === 404;
+
+ if (existingSilenceNotFound) {
+ return ;
}
- return defaults;
-};
-
-const getDefaultFormValues = (searchParams: URLSearchParams, silence?: Silence): SilenceFormFields => {
- const now = new Date();
- if (silence) {
- const isExpired = Date.parse(silence.endsAt) < Date.now();
- const interval = isExpired
- ? {
- start: now,
- end: addDurationToDate(now, { hours: 2 }),
- }
- : { start: new Date(silence.startsAt), end: new Date(silence.endsAt) };
- return {
- id: silence.id,
- startsAt: interval.start.toISOString(),
- endsAt: interval.end.toISOString(),
- comment: silence.comment,
- createdBy: silence.createdBy,
- duration: intervalToAbbreviatedDurationString(interval),
- isRegex: false,
- matchers: silence.matchers?.map(matcherToMatcherField) || [],
- matcherName: '',
- matcherValue: '',
- timeZone: DefaultTimeZone,
- };
- } else {
- const endsAt = addDurationToDate(now, { hours: 2 }); // Default time period is now + 2h
- return {
- id: '',
- startsAt: now.toISOString(),
- endsAt: endsAt.toISOString(),
- comment: `created ${dateTime().format('YYYY-MM-DD HH:mm')}`,
- createdBy: config.bootData.user.name,
- duration: '2h',
- isRegex: false,
- matchers: [{ name: '', value: '', operator: MatcherOperator.equal }],
- matcherName: '',
- matcherValue: '',
- timeZone: DefaultTimeZone,
- ...defaultsFromQuery(searchParams),
- };
- }
-};
-
-export const SilencesEditor = ({ silenceId, alertManagerSourceName }: Props) => {
- // Use a lazy query to fetch the Silence info, as we may not always require this
- // (e.g. if creating a new one from scratch, we don't need to fetch anything)
- const [getSilence, { data: silence, isLoading: getSilenceIsLoading, error: errorGettingExistingSilence }] =
- alertSilencesApi.endpoints.getSilence.useLazyQuery();
- const [createSilence, { isLoading }] = alertSilencesApi.endpoints.createSilence.useMutation();
- const [urlSearchParams] = useURLSearchParams();
-
- const defaultValues = useMemo(() => getDefaultFormValues(urlSearchParams, silence), [silence, urlSearchParams]);
- const formAPI = useForm({ defaultValues });
-
- const styles = useStyles2(getStyles);
- const [matchersForPreview, setMatchersForPreview] = useState(
- defaultValues.matchers.map(matcherFieldToMatcher)
+ return (
+
);
+};
- const { register, handleSubmit, formState, watch, setValue, clearErrors, reset } = formAPI;
+type SilencesEditorProps = {
+ formValues?: SilenceFormFields;
+ alertManagerSourceName: string;
+ onSilenceCreated?: (response: SilenceCreatedResponse) => void;
+ onCancel?: () => void;
+ ruleUid?: string;
+};
+
+/**
+ * Base silences editor used for new silences (from both the list view and the drawer),
+ * and for editing existing silences
+ */
+export const SilencesEditor = ({
+ formValues = getDefaultSilenceFormValues(),
+ alertManagerSourceName,
+ onSilenceCreated,
+ onCancel,
+ ruleUid,
+}: SilencesEditorProps) => {
+ const [createSilence, { isLoading }] = alertSilencesApi.endpoints.createSilence.useMutation();
+ const formAPI = useForm({ defaultValues: formValues });
+ const styles = useStyles2(getStyles);
+
+ const { register, handleSubmit, formState, watch, setValue, clearErrors } = formAPI;
+
+ const [duration, startsAt, endsAt, matchers] = watch(['duration', 'startsAt', 'endsAt', 'matchers']);
+
+ /** Default action taken after creation or cancellation, if corresponding method is not defined */
+ const defaultHandler = () => {
+ locationService.push(makeAMLink('/alerting/silences', alertManagerSourceName));
+ };
+
+ const onSilenceCreatedHandler = onSilenceCreated || defaultHandler;
+ const onCancelHandler = onCancel || defaultHandler;
const onSubmit = async (data: SilenceFormFields) => {
const { id, startsAt, endsAt, comment, createdBy, matchers: matchersFields } = data;
- const matchers = matchersFields.map(matcherFieldToMatcher);
+
+ if (ruleUid) {
+ matchersFields.push({ name: MATCHER_ALERT_RULE_UID, value: ruleUid, operator: MatcherOperator.equal });
+ }
+
+ const matchersToSend = matchersFields.map(matcherFieldToMatcher).filter((field) => field.name && field.value);
const payload = pickBy(
{
id,
@@ -132,35 +135,17 @@ export const SilencesEditor = ({ silenceId, alertManagerSourceName }: Props) =>
endsAt,
comment,
createdBy,
- matchers,
+ matchers: matchersToSend,
},
(value) => !!value
) as SilenceCreatePayload;
await createSilence({ datasourceUid: getDatasourceAPIUid(alertManagerSourceName), payload })
.unwrap()
- .then(() => {
- locationService.push(makeAMLink('/alerting/silences', alertManagerSourceName));
+ .then((newSilenceResponse) => {
+ onSilenceCreatedHandler?.(newSilenceResponse);
});
};
- const duration = watch('duration');
- const startsAt = watch('startsAt');
- const endsAt = watch('endsAt');
- const matcherFields = watch('matchers');
-
- useEffect(() => {
- if (silence) {
- // Allows the form to correctly initialise when an existing silence is fetch from the backend
- reset(getDefaultFormValues(urlSearchParams, silence));
- }
- }, [reset, silence, urlSearchParams]);
-
- useEffect(() => {
- if (silenceId) {
- getSilence({ id: silenceId, datasourceUid: getDatasourceAPIUid(alertManagerSourceName) });
- }
- }, [alertManagerSourceName, getSilence, silenceId]);
-
// Keep duration and endsAt in sync
const [prevDuration, setPrevDuration] = useState(duration);
useDebounce(
@@ -186,37 +171,13 @@ export const SilencesEditor = ({ silenceId, alertManagerSourceName }: Props) =>
700,
[clearErrors, duration, endsAt, prevDuration, setValue, startsAt]
);
-
- useDebounce(
- () => {
- // React-hook-form watch does not return referentialy equal values so this trick is needed
- const newMatchers = matcherFields.filter((m) => m.name && m.value).map(matcherFieldToMatcher);
- if (!isEqual(matchersForPreview, newMatchers)) {
- setMatchersForPreview(newMatchers);
- }
- },
- 700,
- [matcherFields]
- );
-
const userLogged = Boolean(config.bootData.user.isSignedIn && config.bootData.user.name);
- if (getSilenceIsLoading) {
- return ;
- }
-
- const existingSilenceNotFound =
- isFetchError(errorGettingExistingSilence) && errorGettingExistingSilence.status === 404;
-
- if (existingSilenceNotFound) {
- return ;
- }
-
return (