From ed3c36bb46793afdb29300b955a066cdf705d52f Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:21:00 +0100 Subject: [PATCH] =?UTF-8?q?Alerting:=20Use=20time=5Fintervals=20instead=20?= =?UTF-8?q?of=20the=20deprecated=20mute=5Ftime=5Fintervals=20in=20a?= =?UTF-8?q?=E2=80=A6=20(#83147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use time_intervals instead of the deprecated mute_time_intervals in alert manager config * don't send mute_time_intervals in the payload * Add and update tests * Fix usecase when having both fields in response from getting alert manager config * Use mute_timings for grafana data source and both for cloud data source when deleting mute timing * Use mute_timings for grafana data source and both for cloud data source when saving a new or existing alert rule * Address first code review * Address more review comments --- .../alerting/unified/MuteTimings.test.tsx | 159 +++++++++++++++++- .../features/alerting/unified/MuteTimings.tsx | 15 +- .../alerting/unified/NotificationPolicies.tsx | 4 +- .../mute-timings/MuteTimingForm.tsx | 91 ++++++++-- .../mute-timings/MuteTimingsTable.tsx | 8 +- .../unified/components/mute-timings/util.tsx | 10 +- .../unified/hooks/useMuteTimingOptions.ts | 4 +- .../alerting/unified/state/actions.ts | 22 ++- .../plugins/datasource/alertmanager/types.ts | 1 + 9 files changed, 279 insertions(+), 35 deletions(-) diff --git a/public/app/features/alerting/unified/MuteTimings.test.tsx b/public/app/features/alerting/unified/MuteTimings.test.tsx index e87db13b33c..b54591c258e 100644 --- a/public/app/features/alerting/unified/MuteTimings.test.tsx +++ b/public/app/features/alerting/unified/MuteTimings.test.tsx @@ -1,4 +1,4 @@ -import { render, waitFor, fireEvent, within } from '@testing-library/react'; +import { fireEvent, render, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; @@ -10,7 +10,7 @@ import { AccessControlAction } from 'app/types'; import MuteTimings from './MuteTimings'; import { fetchAlertManagerConfig, updateAlertManagerConfig } from './api/alertmanager'; -import { grantUserPermissions, mockDataSource, MockDataSourceSrv } from './mocks'; +import { MockDataSourceSrv, grantUserPermissions, mockDataSource } from './mocks'; import { DataSourceType } from './utils/datasource'; jest.mock('./api/alertmanager'); @@ -71,6 +71,21 @@ const muteTimeInterval: MuteTimeInterval = { }, ], }; +const muteTimeInterval2: MuteTimeInterval = { + name: 'default-mute2', + time_intervals: [ + { + times: [ + { + start_time: '12:00', + end_time: '24:00', + }, + ], + days_of_month: ['15', '-1'], + months: ['august:december', 'march'], + }, + ], +}; const defaultConfig: AlertManagerCortexConfig = { alertmanager_config: { @@ -90,6 +105,44 @@ const defaultConfig: AlertManagerCortexConfig = { }, template_files: {}, }; +const defaultConfigWithNewTimeIntervalsField: AlertManagerCortexConfig = { + alertmanager_config: { + receivers: [{ name: 'default' }, { name: 'critical' }], + route: { + receiver: 'default', + group_by: ['alertname'], + routes: [ + { + matchers: ['env=prod', 'region!=EU'], + mute_time_intervals: [muteTimeInterval.name], + }, + ], + }, + templates: [], + time_intervals: [muteTimeInterval], + }, + template_files: {}, +}; + +const defaultConfigWithBothTimeIntervalsField: AlertManagerCortexConfig = { + alertmanager_config: { + receivers: [{ name: 'default' }, { name: 'critical' }], + route: { + receiver: 'default', + group_by: ['alertname'], + routes: [ + { + matchers: ['env=prod', 'region!=EU'], + mute_time_intervals: [muteTimeInterval.name], + }, + ], + }, + templates: [], + time_intervals: [muteTimeInterval], + mute_time_intervals: [muteTimeInterval2], + }, + template_files: {}, +}; const resetMocks = () => { jest.resetAllMocks(); @@ -110,7 +163,7 @@ describe('Mute timings', () => { grantUserPermissions(Object.values(AccessControlAction)); }); - it('creates a new mute timing', async () => { + it('creates a new mute timing, with mute_time_intervals in config', async () => { renderMuteTimings(); await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled()); @@ -125,10 +178,12 @@ describe('Mute timings', () => { fireEvent.submit(ui.form.get()); await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled()); + + const { mute_time_intervals: _, ...configWithoutMuteTimings } = defaultConfig.alertmanager_config; expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', { ...defaultConfig, alertmanager_config: { - ...defaultConfig.alertmanager_config, + ...configWithoutMuteTimings, mute_time_intervals: [ muteTimeInterval, { @@ -151,6 +206,102 @@ describe('Mute timings', () => { }); }); + it('creates a new mute timing, with time_intervals in config', async () => { + mocks.api.fetchAlertManagerConfig.mockImplementation(() => { + return Promise.resolve({ + ...defaultConfigWithNewTimeIntervalsField, + }); + }); + renderMuteTimings(); + + await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled()); + expect(ui.nameField.get()).toBeInTheDocument(); + + await userEvent.type(ui.nameField.get(), 'maintenance period'); + await userEvent.type(ui.startsAt.get(), '22:00'); + await userEvent.type(ui.endsAt.get(), '24:00'); + await userEvent.type(ui.days.get(), '-1'); + await userEvent.type(ui.months.get(), 'january, july'); + + fireEvent.submit(ui.form.get()); + + await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled()); + + const { mute_time_intervals: _, ...configWithoutMuteTimings } = defaultConfig.alertmanager_config; + expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', { + ...defaultConfig, + alertmanager_config: { + ...configWithoutMuteTimings, + mute_time_intervals: [ + muteTimeInterval, + { + name: 'maintenance period', + time_intervals: [ + { + days_of_month: ['-1'], + months: ['january', 'july'], + times: [ + { + start_time: '22:00', + end_time: '24:00', + }, + ], + }, + ], + }, + ], + }, + }); + }); + it('creates a new mute timing, with time_intervals and mute_time_intervals in config', async () => { + mocks.api.fetchAlertManagerConfig.mockImplementation(() => { + return Promise.resolve({ + ...defaultConfigWithBothTimeIntervalsField, + }); + }); + renderMuteTimings(); + + await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled()); + expect(ui.nameField.get()).toBeInTheDocument(); + + await userEvent.type(ui.nameField.get(), 'maintenance period'); + await userEvent.type(ui.startsAt.get(), '22:00'); + await userEvent.type(ui.endsAt.get(), '24:00'); + await userEvent.type(ui.days.get(), '-1'); + await userEvent.type(ui.months.get(), 'january, july'); + + fireEvent.submit(ui.form.get()); + + await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled()); + + const { mute_time_intervals, time_intervals, ...configWithoutMuteTimings } = defaultConfig.alertmanager_config; + expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith('grafana', { + ...defaultConfig, + alertmanager_config: { + ...configWithoutMuteTimings, + mute_time_intervals: [ + muteTimeInterval, + muteTimeInterval2, + { + name: 'maintenance period', + time_intervals: [ + { + days_of_month: ['-1'], + months: ['january', 'july'], + times: [ + { + start_time: '22:00', + end_time: '24:00', + }, + ], + }, + ], + }, + ], + }, + }); + }); + it('prepopulates the form when editing a mute timing', async () => { renderMuteTimings('/alerting/routes/mute-timing/edit' + `?muteName=${encodeURIComponent(muteTimeInterval.name)}`); diff --git a/public/app/features/alerting/unified/MuteTimings.tsx b/public/app/features/alerting/unified/MuteTimings.tsx index c4d1584380c..a49205c4774 100644 --- a/public/app/features/alerting/unified/MuteTimings.tsx +++ b/public/app/features/alerting/unified/MuteTimings.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { Route, Redirect, Switch, useRouteMatch } from 'react-router-dom'; +import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; import { NavModelItem } from '@grafana/data'; import { Alert } from '@grafana/ui'; @@ -21,8 +21,9 @@ const MuteTimings = () => { const config = currentData?.alertmanager_config; const getMuteTimingByName = useCallback( - (id: string): MuteTimeInterval | undefined => { - const timing = config?.mute_time_intervals?.find(({ name }: MuteTimeInterval) => name === id); + (id: string, fromTimeIntervals: boolean): MuteTimeInterval | undefined => { + const time_intervals = fromTimeIntervals ? config?.time_intervals ?? [] : config?.mute_time_intervals ?? []; + const timing = time_intervals.find(({ name }: MuteTimeInterval) => name === id); if (timing) { const provenance = config?.muteTimeProvenances?.[timing.name]; @@ -53,13 +54,17 @@ const MuteTimings = () => { {() => { if (queryParams['muteName']) { - const muteTiming = getMuteTimingByName(String(queryParams['muteName'])); + const muteTimingInMuteTimings = getMuteTimingByName(String(queryParams['muteName']), false); + const muteTimingInTimeIntervals = getMuteTimingByName(String(queryParams['muteName']), true); + const inTimeIntervals = Boolean(muteTimingInTimeIntervals); + const muteTiming = inTimeIntervals ? muteTimingInTimeIntervals : muteTimingInMuteTimings; const provenance = muteTiming?.provenance; return ( diff --git a/public/app/features/alerting/unified/NotificationPolicies.tsx b/public/app/features/alerting/unified/NotificationPolicies.tsx index b1696dbb2a4..16017ecc8e1 100644 --- a/public/app/features/alerting/unified/NotificationPolicies.tsx +++ b/public/app/features/alerting/unified/NotificationPolicies.tsx @@ -15,6 +15,7 @@ import { useGetContactPointsState } from './api/receiversApi'; import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; import { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable'; +import { mergeTimeIntervals } from './components/mute-timings/util'; import { NotificationPoliciesFilter, findRoutesByMatchers, @@ -191,8 +192,9 @@ const AmRoutes = () => { if (!selectedAlertmanager) { return null; } + const time_intervals = result?.alertmanager_config ? mergeTimeIntervals(result?.alertmanager_config) : []; - const numberOfMuteTimings = result?.alertmanager_config.mute_time_intervals?.length ?? 0; + const numberOfMuteTimings = time_intervals.length; const haveData = result && !resultError && !resultLoading; const isFetching = !result && resultLoading; const haveError = resultError && !resultLoading; diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx index 781c25ede1b..36cc86dad8a 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx @@ -12,6 +12,7 @@ import { useAlertmanager } from '../../state/AlertmanagerContext'; import { updateAlertManagerConfigAction } from '../../state/actions'; import { MuteTimingFields } from '../../types/mute-timing-form'; import { renameMuteTimings } from '../../utils/alertmanager'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { makeAMLink } from '../../utils/misc'; import { createMuteTiming, defaultTimeInterval } from '../../utils/mute-timings'; import { ProvisionedResource, ProvisioningAlert } from '../Provisioning'; @@ -19,7 +20,8 @@ import { ProvisionedResource, ProvisioningAlert } from '../Provisioning'; import { MuteTimingTimeInterval } from './MuteTimingTimeInterval'; interface Props { - muteTiming?: MuteTimeInterval; + fromLegacyTimeInterval?: MuteTimeInterval; // mute time interval when comes from the old config , mute_time_intervals + fromTimeIntervals?: MuteTimeInterval; // mute time interval when comes from the new config , time_intervals. These two fields are mutually exclusive showError?: boolean; provenance?: string; loading?: boolean; @@ -50,7 +52,26 @@ const useDefaultValues = (muteTiming?: MuteTimeInterval): MuteTimingFields => { }; }; -const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) => { +const replaceMuteTiming = ( + originalTimings: MuteTimeInterval[], + existingTiming: MuteTimeInterval | undefined, + newTiming: MuteTimeInterval, + addNew: boolean +) => { + // we only add new timing if addNew is true. Otherwise, we just remove the existing timing + const originalTimingsWithoutNew = existingTiming + ? originalTimings?.filter(({ name }) => name !== existingTiming.name) + : originalTimings; + return addNew ? [...originalTimingsWithoutNew, newTiming] : [...originalTimingsWithoutNew]; +}; + +const MuteTimingForm = ({ + fromLegacyTimeInterval: fromMuteTimings, + fromTimeIntervals, + showError, + loading, + provenance, +}: Props) => { const dispatch = useDispatch(); const { selectedAlertmanager } = useAlertmanager(); const styles = useStyles2(getStyles); @@ -60,6 +81,12 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) = const { currentData: result } = useAlertmanagerConfig(selectedAlertmanager); const config = result?.alertmanager_config; + const fromIntervals = Boolean(fromTimeIntervals); + const muteTiming = fromIntervals ? fromTimeIntervals : fromMuteTimings; + + const originalMuteTimings = config?.mute_time_intervals ?? []; + const originalTimeIntervals = config?.time_intervals ?? []; + const defaultValues = useDefaultValues(muteTiming); const formApi = useForm({ defaultValues }); @@ -70,19 +97,44 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) = const newMuteTiming = createMuteTiming(values); - const muteTimings = muteTiming - ? config?.mute_time_intervals?.filter(({ name }) => name !== muteTiming.name) - : config?.mute_time_intervals; + const isGrafanaDataSource = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME; + const isNewMuteTiming = fromTimeIntervals === undefined && fromMuteTimings === undefined; + // If is Grafana data source, we wil save mute timings in the alertmanager_config.mute_time_intervals + // Otherwise, we will save it on alertmanager_config.time_intervals or alertmanager_config.mute_time_intervals depending on the original config + + const newMutetimeIntervals = isGrafanaDataSource + ? { + // for Grafana data source, we will save mute timings in the alertmanager_config.mute_time_intervals + mute_time_intervals: [ + ...replaceMuteTiming(originalTimeIntervals, fromTimeIntervals, newMuteTiming, false), + ...replaceMuteTiming(originalMuteTimings, fromMuteTimings, newMuteTiming, true), + ], + } + : { + // for non-Grafana data source, we will save mute timings in the alertmanager_config.time_intervals or alertmanager_config.mute_time_intervals depending on the original config + time_intervals: replaceMuteTiming( + originalTimeIntervals, + fromTimeIntervals, + newMuteTiming, + Boolean(fromTimeIntervals) || isNewMuteTiming + ), + mute_time_intervals: + Boolean(fromMuteTimings) && !isNewMuteTiming + ? replaceMuteTiming(originalMuteTimings, fromMuteTimings, newMuteTiming, true) + : undefined, + }; + + const { mute_time_intervals: _, time_intervals: __, ...configWithoutMuteTimings } = config ?? {}; const newConfig: AlertManagerCortexConfig = { ...result, alertmanager_config: { - ...config, + ...configWithoutMuteTimings, route: muteTiming && newMuteTiming.name !== muteTiming.name ? renameMuteTimings(newMuteTiming.name, muteTiming.name, config?.route ?? {}) : config?.route, - mute_time_intervals: [...(muteTimings || []), newMuteTiming], + ...newMutetimeIntervals, }, }; @@ -123,13 +175,8 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) = { - if (!muteTiming) { - const existingMuteTiming = config?.mute_time_intervals?.find(({ name }) => value === name); - return existingMuteTiming ? `Mute timing already exists for "${value}"` : true; - } - return; - }, + validate: (value) => + validateMuteTiming(value, muteTiming, originalMuteTimings, originalTimeIntervals), })} className={styles.input} data-testid={'mute-timing-name'} @@ -156,6 +203,22 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) = ); }; +function validateMuteTiming( + value: string, + muteTiming: MuteTimeInterval | undefined, + originalMuteTimings: MuteTimeInterval[], + originalTimeIntervals: MuteTimeInterval[] +) { + if (!muteTiming) { + const existingMuteTimingInMuteTimings = originalMuteTimings?.find(({ name }) => value === name); + const existingMuteTimingInTimeIntervals = originalTimeIntervals?.find(({ name }) => value === name); + return existingMuteTimingInMuteTimings || existingMuteTimingInTimeIntervals + ? `Mute timing already exists for "${value}"` + : true; + } + return; +} + const getStyles = (theme: GrafanaTheme2) => ({ input: css` width: 400px; diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx index 5db1a2ce98a..7e13eddc50d 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx @@ -18,7 +18,7 @@ import { ProvisioningBadge } from '../Provisioning'; import { Spacer } from '../Spacer'; import { GrafanaMuteTimingsExporter } from '../export/GrafanaMuteTimingsExporter'; -import { renderTimeIntervals } from './util'; +import { mergeTimeIntervals, renderTimeIntervals } from './util'; const ALL_MUTE_TIMINGS = Symbol('all mute timings'); @@ -72,9 +72,9 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide const config = currentData?.alertmanager_config; const [muteTimingName, setMuteTimingName] = useState(''); - const items = useMemo((): Array> => { - const muteTimings = config?.mute_time_intervals ?? []; + // merge both fields mute_time_intervals and time_intervals to support both old and new config + const muteTimings = config ? mergeTimeIntervals(config) : []; const muteTimingsProvenances = config?.muteTimeProvenances ?? {}; return muteTimings @@ -88,7 +88,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide }, }; }); - }, [config?.mute_time_intervals, config?.muteTimeProvenances, muteTimingNames]); + }, [muteTimingNames, config]); const [_, allowedToCreateMuteTiming] = useAlertmanagerAbility(AlertmanagerAction.CreateMuteTiming); diff --git a/public/app/features/alerting/unified/components/mute-timings/util.tsx b/public/app/features/alerting/unified/components/mute-timings/util.tsx index dda90ae1b39..00a52f5e8ff 100644 --- a/public/app/features/alerting/unified/components/mute-timings/util.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/util.tsx @@ -1,7 +1,7 @@ import moment from 'moment'; import React from 'react'; -import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; +import { AlertmanagerConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; import { getDaysOfMonthString, @@ -18,6 +18,12 @@ const isvalidTimeFormat = (timeString: string): boolean => { return timeString ? TIME_RANGE_REGEX.test(timeString) : true; }; +// merge both fields mute_time_intervals and time_intervals to support both old and new config +export const mergeTimeIntervals = (alertManagerConfig: AlertmanagerConfig) => { + return [...(alertManagerConfig.mute_time_intervals ?? []), ...(alertManagerConfig.time_intervals ?? [])]; +}; + +// Usage const isValidStartAndEndTime = (startTime?: string, endTime?: string): boolean => { // empty time range is perfactly valid for a mute timing if (!startTime && !endTime) { @@ -67,4 +73,4 @@ function renderTimeIntervals(muteTiming: MuteTimeInterval) { }); } -export { isvalidTimeFormat, isValidStartAndEndTime, renderTimeIntervals }; +export { isValidStartAndEndTime, isvalidTimeFormat, renderTimeIntervals }; diff --git a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts index 675c9219758..777f615edec 100644 --- a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts +++ b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; +import { mergeTimeIntervals } from '../components/mute-timings/util'; import { useAlertmanager } from '../state/AlertmanagerContext'; import { timeIntervalToString } from '../utils/alertmanager'; @@ -13,8 +14,9 @@ export function useMuteTimingOptions(): Array> { const config = currentData?.alertmanager_config; return useMemo(() => { + const time_intervals = config ? mergeTimeIntervals(config) : []; const muteTimingsOptions: Array> = - config?.mute_time_intervals?.map((value) => ({ + time_intervals?.map((value) => ({ value: value.name, label: value.name, description: value.time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND '), diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 0035acdcecf..0daafcaca65 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -693,10 +693,24 @@ export const deleteMuteTimingAction = (alertManagerSourceName: string, muteTimin alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName) ).unwrap(); - const muteIntervals = - config?.alertmanager_config?.mute_time_intervals?.filter(({ name }) => name !== muteTimingName) ?? []; + const isGrafanaDatasource = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME; + + const muteIntervalsFiltered = + (config?.alertmanager_config?.mute_time_intervals ?? [])?.filter(({ name }) => name !== muteTimingName) ?? []; + const timeIntervalsFiltered = + (config?.alertmanager_config?.time_intervals ?? [])?.filter(({ name }) => name !== muteTimingName) ?? []; + + const time_intervals_without_mute_to_save = isGrafanaDatasource + ? { + mute_time_intervals: [...muteIntervalsFiltered, ...timeIntervalsFiltered], + } + : { + time_intervals: timeIntervalsFiltered, + mute_time_intervals: muteIntervalsFiltered, + }; if (config) { + const { mute_time_intervals: _, ...configWithoutMuteTimings } = config?.alertmanager_config ?? {}; withAppEvents( dispatch( updateAlertManagerConfigAction({ @@ -705,11 +719,11 @@ export const deleteMuteTimingAction = (alertManagerSourceName: string, muteTimin newConfig: { ...config, alertmanager_config: { - ...config.alertmanager_config, + ...configWithoutMuteTimings, route: config.alertmanager_config.route ? removeMuteTimingFromRoute(muteTimingName, config.alertmanager_config?.route) : undefined, - mute_time_intervals: muteIntervals, + ...time_intervals_without_mute_to_save, }, }, }) diff --git a/public/app/plugins/datasource/alertmanager/types.ts b/public/app/plugins/datasource/alertmanager/types.ts index 4921c31a9c2..059773ac0d0 100644 --- a/public/app/plugins/datasource/alertmanager/types.ts +++ b/public/app/plugins/datasource/alertmanager/types.ts @@ -157,6 +157,7 @@ export type AlertmanagerConfig = { inhibit_rules?: InhibitRule[]; receivers?: Receiver[]; mute_time_intervals?: MuteTimeInterval[]; + time_intervals?: MuteTimeInterval[]; /** { [name]: provenance } */ muteTimeProvenances?: Record; last_applied?: boolean;