From 0a2db346ab076a011c76988ea44e82aa61d73b25 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Tue, 13 Aug 2024 17:38:40 +0200 Subject: [PATCH] Alerting: use new Alertmanager update hooks for mute and active time intervals (#91404) --- .../alerting/unified/MuteTimings.test.tsx | 15 +- .../mute-timings/MuteTimingActionsButtons.tsx | 27 +- .../mute-timings/MuteTimingForm.tsx | 12 +- .../mute-timings/MuteTimingsTable.test.tsx | 5 +- .../mute-timings/useMuteTimings.tsx | 144 +++----- .../EditNotificationPolicyForm.tsx | 23 +- .../notification-policies/Policy.tsx | 22 +- .../unified/hooks/mergeRequestStates.tsx | 19 + .../unified/hooks/useMuteTimingOptions.ts | 27 -- .../hooks/useProduceNewAlertmanagerConfig.ts | 4 +- .../__snapshots__/muteTimings.test.ts.snap | 329 ++++++++++++++++++ .../reducers/alertmanager/muteTimings.test.ts | 84 +++++ .../reducers/alertmanager/muteTimings.ts | 75 ++++ .../alerting/unified/state/actions.ts | 50 +-- 14 files changed, 636 insertions(+), 200 deletions(-) create mode 100644 public/app/features/alerting/unified/hooks/mergeRequestStates.tsx delete mode 100644 public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts create mode 100644 public/app/features/alerting/unified/reducers/alertmanager/__snapshots__/muteTimings.test.ts.snap create mode 100644 public/app/features/alerting/unified/reducers/alertmanager/muteTimings.test.ts create mode 100644 public/app/features/alerting/unified/reducers/alertmanager/muteTimings.ts diff --git a/public/app/features/alerting/unified/MuteTimings.test.tsx b/public/app/features/alerting/unified/MuteTimings.test.tsx index b6e0668990c..788d64b3dcb 100644 --- a/public/app/features/alerting/unified/MuteTimings.test.tsx +++ b/public/app/features/alerting/unified/MuteTimings.test.tsx @@ -1,4 +1,5 @@ import { InitialEntry } from 'history'; +import { last } from 'lodash'; import { Route } from 'react-router'; import { render, within, userEvent, screen } from 'test/test-utils'; import { byRole, byTestId, byText } from 'testing-library-selector'; @@ -189,7 +190,7 @@ const saveMuteTiming = async () => { setupMswServer(); -const getAlertmanagerConfigUpdate = async (requests: Request[]) => { +const getAlertmanagerConfigUpdate = async (requests: Request[]): Promise => { const alertmanagerUpdate = requests.find( (r) => r.url.match('/alertmanager/(.*)/config/api/v1/alert') && r.method === 'POST' ); @@ -227,8 +228,10 @@ describe('Mute timings', () => { const requests = await capture; const alertmanagerUpdate = await getAlertmanagerConfigUpdate(requests); + const lastAdded = last(alertmanagerUpdate.alertmanager_config.time_intervals); + // Check that the last mute_time_interval is the one we just submitted via the form - expect(alertmanagerUpdate.alertmanager_config.mute_time_intervals.pop().name).toEqual('maintenance period'); + expect(lastAdded?.name).toEqual('maintenance period'); }); it('creates a new mute timing, with time_intervals in config', async () => { @@ -252,7 +255,9 @@ describe('Mute timings', () => { const requests = await capture; const alertmanagerUpdate = await getAlertmanagerConfigUpdate(requests); - expect(alertmanagerUpdate.alertmanager_config.time_intervals.pop().name).toEqual('maintenance period'); + const lastAdded = last(alertmanagerUpdate.alertmanager_config.time_intervals); + + expect(lastAdded?.name).toEqual('maintenance period'); }); it('creates a new mute timing, with time_intervals and mute_time_intervals in config', async () => { @@ -310,9 +315,9 @@ describe('Mute timings', () => { const requests = await capture; const alertmanagerUpdate = await getAlertmanagerConfigUpdate(requests); - const mostRecentInterval = alertmanagerUpdate.alertmanager_config.mute_time_intervals.pop().time_intervals[0]; + const lastAdded = last(alertmanagerUpdate.alertmanager_config.mute_time_intervals); - expect(mostRecentInterval).toMatchObject({ + expect(lastAdded?.time_intervals[0]).toMatchObject({ days_of_month: [formValues.days], months: formValues.months.split(', '), years: [formValues.years], diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons.tsx index f6e9c176b1a..05ce0335f86 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons.tsx @@ -6,6 +6,7 @@ import { useExportMuteTimingsDrawer } from 'app/features/alerting/unified/compon import { Authorize } from '../../components/Authorize'; import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; +import { isLoading } from '../../hooks/useAsync'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { makeAMLink } from '../../utils/misc'; import { isDisabled } from '../../utils/mute-timings'; @@ -18,7 +19,9 @@ interface MuteTimingActionsButtonsProps { } export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }: MuteTimingActionsButtonsProps) => { - const deleteMuteTiming = useDeleteMuteTiming({ alertmanager: alertManagerSourceName! }); + const [deleteMuteTiming, deleteMuteTimingRequestState] = useDeleteMuteTiming({ + alertmanager: alertManagerSourceName!, + }); const [showDeleteDrawer, setShowDeleteDrawer] = useState(false); const [ExportDrawer, showExportDrawer] = useExportMuteTimingsDrawer(); const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportMuteTimings); @@ -31,7 +34,13 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }: }); const viewOrEditButton = ( - + {muteTiming.provisioned ? ( View ) : ( @@ -52,7 +61,7 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }: variant="secondary" size="sm" data-testid="export" - disabled={!exportAllowed} + disabled={!exportAllowed || isLoading(deleteMuteTimingRequestState)} onClick={() => showExportDrawer(muteTiming.name)} > Export @@ -61,7 +70,13 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }: {!muteTiming.provisioned && ( - setShowDeleteDrawer(true)}> + setShowDeleteDrawer(true)} + disabled={isLoading(deleteMuteTimingRequestState)} + > Delete @@ -73,8 +88,8 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }: body={`Are you sure you would like to delete "${muteTiming.name}"?`} confirmText={t('alerting.common.delete', 'Delete')} onConfirm={async () => { - await deleteMuteTiming({ - name: muteTiming?.metadata?.name || muteTiming.name, + await deleteMuteTiming.execute({ + name: muteTiming?.metadata?.name ?? muteTiming.name, }); closeDeleteModal(); 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 5be3a7e3cae..df15dd85679 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx @@ -60,9 +60,11 @@ const useDefaultValues = (muteTiming?: MuteTiming): MuteTimingFields => { const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode }: Props) => { const { selectedAlertmanager } = useAlertmanager(); const hookArgs = { alertmanager: selectedAlertmanager! }; - const createTimeInterval = useCreateMuteTiming(hookArgs); - const updateTimeInterval = useUpdateMuteTiming(hookArgs); + + const [createTimeInterval] = useCreateMuteTiming(hookArgs); + const [updateTimeInterval] = useUpdateMuteTiming(hookArgs); const validateMuteTiming = useValidateMuteTiming(hookArgs); + /** * The k8s API approach does not support renaming an entity at this time, * as it requires renaming all other references of this entity. @@ -79,13 +81,13 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode const returnLink = makeAMLink('/alerting/routes/', selectedAlertmanager!, { tab: 'mute_timings' }); const onSubmit = async (values: MuteTimingFields) => { - const timeInterval = createMuteTiming(values); + const interval = createMuteTiming(values); const updateOrCreate = async () => { if (editMode) { - return updateTimeInterval({ timeInterval, originalName: muteTiming?.metadata?.name || muteTiming!.name }); + return updateTimeInterval.execute({ interval, originalName: muteTiming?.metadata?.name || muteTiming!.name }); } - return createTimeInterval({ timeInterval }); + return createTimeInterval.execute({ interval }); }; return updateOrCreate().then(() => { diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx index e11e628b351..cac835fad21 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx @@ -8,10 +8,11 @@ import { setMuteTimingsListError, } from 'app/features/alerting/unified/mocks/server/configure'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; -import { TIME_INTERVAL_UID_HAPPY_PATH } from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s'; +import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import { grantUserPermissions } from '../../mocks'; +import { TIME_INTERVAL_UID_HAPPY_PATH } from '../../mocks/server/handlers/k8s/timeIntervals.k8s'; import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; @@ -80,7 +81,7 @@ describe('MuteTimingsTable', () => { (r) => r.url.includes('/alertmanager/grafana/config/api/v1/alerts') && r.method === 'POST' ); - const body = await amConfigUpdateRequest?.clone().json(); + const body: AlertManagerCortexConfig = await amConfigUpdateRequest?.clone().json(); expect(body.alertmanager_config.mute_time_intervals).toHaveLength(0); }); diff --git a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx index 6d13a6037a8..0e579fa3dcb 100644 --- a/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx @@ -1,4 +1,3 @@ -import { produce } from 'immer'; import { useEffect } from 'react'; import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi'; @@ -8,14 +7,18 @@ import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval, ReadNamespacedTimeIntervalApiResponse, } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen'; -import { deleteMuteTimingAction, updateAlertManagerConfigAction } from 'app/features/alerting/unified/state/actions'; import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks'; -import { renameTimeInterval } from 'app/features/alerting/unified/utils/alertmanager'; -import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; import { getK8sNamespace, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; + +import { useAsync } from '../../hooks/useAsync'; +import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig'; +import { + addMuteTimingAction, + deleteMuteTimingAction, + updateMuteTimingAction, +} from '../../reducers/alertmanager/muteTimings'; const { useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi; const { @@ -113,6 +116,8 @@ export const useMuteTimings = ({ alertmanager }: BaseAlertmanagerArgs) => { return useK8sApi ? intervalsResponse : configApiResponse; }; +type CreateUpdateMuteTimingArgs = { interval: MuteTimeInterval }; + /** * Create a new mute timing. * @@ -124,40 +129,24 @@ export const useMuteTimings = ({ alertmanager }: BaseAlertmanagerArgs) => { export const useCreateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { const useK8sApi = shouldUseK8sApi(alertmanager); - const dispatch = useDispatch(); const [createGrafanaTimeInterval] = useCreateNamespacedTimeIntervalMutation(); - const [getAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery(); + const [updateConfiguration] = useProduceNewAlertmanagerConfiguration(); - const isGrafanaAm = alertmanager === GRAFANA_RULES_SOURCE_NAME; - - if (useK8sApi) { + const addToK8sAPI = useAsync(({ interval }: CreateUpdateMuteTimingArgs) => { const namespace = getK8sNamespace(); - return ({ timeInterval }: { timeInterval: MuteTimeInterval }) => - createGrafanaTimeInterval({ - namespace, - comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval: { metadata: {}, spec: timeInterval }, - }).unwrap(); - } - return async ({ timeInterval }: { timeInterval: MuteTimeInterval }) => { - const result = await getAlertmanagerConfig(alertmanager).unwrap(); - const newConfig = produce(result, (draft) => { - const propertyToUpdate = isGrafanaAm ? 'mute_time_intervals' : 'time_intervals'; - draft.alertmanager_config[propertyToUpdate] = draft.alertmanager_config[propertyToUpdate] ?? []; - draft.alertmanager_config[propertyToUpdate] = (draft.alertmanager_config[propertyToUpdate] ?? []).concat( - timeInterval - ); - }); - - return dispatch( - updateAlertManagerConfigAction({ - newConfig, - oldConfig: result, - alertManagerSourceName: alertmanager, - successMessage: 'Mute timing saved', - }) - ).unwrap(); - }; + return createGrafanaTimeInterval({ + namespace, + comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval: { metadata: {}, spec: interval }, + }).unwrap(); + }); + + const addToAlertmanagerConfiguration = useAsync(({ interval }: CreateUpdateMuteTimingArgs) => { + const action = addMuteTimingAction({ interval }); + return updateConfiguration(action); + }); + + return useK8sApi ? addToK8sAPI : addToAlertmanagerConfiguration; }; /** @@ -223,84 +212,59 @@ export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertma export const useUpdateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { const useK8sApi = shouldUseK8sApi(alertmanager); - const dispatch = useDispatch(); const [replaceGrafanaTimeInterval] = useReplaceNamespacedTimeIntervalMutation(); - const [getAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery(); + const [updateConfiguration] = useProduceNewAlertmanagerConfiguration(); - if (useK8sApi) { - return async ({ timeInterval, originalName }: { timeInterval: MuteTimeInterval; originalName: string }) => { + const updateToK8sAPI = useAsync( + async ({ interval, originalName }: CreateUpdateMuteTimingArgs & { originalName: string }) => { const namespace = getK8sNamespace(); + return replaceGrafanaTimeInterval({ name: originalName, namespace, comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval: { - spec: timeInterval, + spec: interval, metadata: { name: originalName }, }, }).unwrap(); - }; - } - - return async ({ timeInterval, originalName }: { timeInterval: MuteTimeInterval; originalName: string }) => { - const nameHasChanged = timeInterval.name !== originalName; - const result = await getAlertmanagerConfig(alertmanager).unwrap(); - - const newConfig = produce(result, (draft) => { - const existingIntervalIndex = (draft.alertmanager_config?.time_intervals || [])?.findIndex( - ({ name }) => name === originalName - ); - if (existingIntervalIndex !== -1) { - draft.alertmanager_config.time_intervals![existingIntervalIndex] = timeInterval; - } + } + ); - const existingMuteIntervalIndex = (draft.alertmanager_config?.mute_time_intervals || [])?.findIndex( - ({ name }) => name === originalName - ); - if (existingMuteIntervalIndex !== -1) { - draft.alertmanager_config.mute_time_intervals![existingMuteIntervalIndex] = timeInterval; - } + const updateToAlertmanagerConfiguration = useAsync( + async ({ interval, originalName }: CreateUpdateMuteTimingArgs & { originalName: string }) => { + const action = updateMuteTimingAction({ interval, originalName }); + return updateConfiguration(action); + } + ); - if (nameHasChanged && draft.alertmanager_config.route) { - draft.alertmanager_config.route = renameTimeInterval( - timeInterval.name, - originalName, - draft.alertmanager_config.route - ); - } - }); - - return dispatch( - updateAlertManagerConfigAction({ - newConfig, - oldConfig: result, - alertManagerSourceName: alertmanager, - successMessage: 'Mute timing saved', - }) - ).unwrap(); - }; + return useK8sApi ? updateToK8sAPI : updateToAlertmanagerConfiguration; }; /** * Delete a mute timing interval */ +type DeleteMuteTimingArgs = { name: string }; export const useDeleteMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { const useK8sApi = shouldUseK8sApi(alertmanager); - const dispatch = useDispatch(); + const [updateConfiguration, _updateConfigurationRequestState] = useProduceNewAlertmanagerConfiguration(); const [deleteGrafanaTimeInterval] = useDeleteNamespacedTimeIntervalMutation(); - if (useK8sApi) { - return async ({ name }: { name: string }) => { - const namespace = getK8sNamespace(); - return deleteGrafanaTimeInterval({ - name, - namespace, - ioK8SApimachineryPkgApisMetaV1DeleteOptions: {}, - }).unwrap(); - }; - } + const deleteFromAlertmanagerAPI = useAsync(async ({ name }: DeleteMuteTimingArgs) => { + const action = deleteMuteTimingAction({ name }); + return updateConfiguration(action); + }); + + const deleteFromK8sAPI = useAsync(async ({ name }: DeleteMuteTimingArgs) => { + const namespace = getK8sNamespace(); + await deleteGrafanaTimeInterval({ + name, + namespace, + ioK8SApimachineryPkgApisMetaV1DeleteOptions: {}, + }).unwrap(); + }); - return async ({ name }: { name: string }) => dispatch(deleteMuteTimingAction(alertmanager, name)); + return useK8sApi ? deleteFromK8sAPI : deleteFromAlertmanagerAPI; }; export const useValidateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => { diff --git a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx index e4988b9b59e..d229bcc99fb 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx @@ -1,8 +1,8 @@ import { css } from '@emotion/css'; -import { ReactNode, useState } from 'react'; +import { ReactNode, useMemo, useState } from 'react'; import { useForm, Controller, useFieldArray } from 'react-hook-form'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Badge, Button, @@ -20,9 +20,9 @@ import { ContactPointSelector } from 'app/features/alerting/unified/components/n import { handleContactPointSelect } from 'app/features/alerting/unified/components/notification-policies/utils'; import { MatcherOperator, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; -import { useMuteTimingOptions } from '../../hooks/useMuteTimingOptions'; +import { useAlertmanager } from '../../state/AlertmanagerContext'; import { FormAmRoute } from '../../types/amroutes'; -import { matcherFieldOptions } from '../../utils/alertmanager'; +import { matcherFieldOptions, timeIntervalToString } from '../../utils/alertmanager'; import { amRouteToFormAmRoute, commonGroupByOptions, @@ -33,6 +33,7 @@ import { stringToSelectableValue, stringsToSelectableValues, } from '../../utils/amroutes'; +import { useMuteTimings } from '../mute-timings/useMuteTimings'; import { PromDurationInput } from './PromDurationInput'; import { getFormStyles } from './formStyles'; @@ -48,9 +49,21 @@ export interface AmRoutesExpandedFormProps { export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults }: AmRoutesExpandedFormProps) => { const styles = useStyles2(getStyles); const formStyles = useStyles2(getFormStyles); + const { selectedAlertmanager } = useAlertmanager(); const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(route?.group_by)); - const muteTimingOptions = useMuteTimingOptions(); const emptyMatcher = [{ name: '', operator: MatcherOperator.equal, value: '' }]; + const { data: muteTimings } = useMuteTimings({ alertmanager: selectedAlertmanager! }); + + const muteTimingOptions = useMemo(() => { + const muteTimingsOptions: Array> = + muteTimings?.map((value) => ({ + value: value.name, + label: value.name, + description: value.time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND '), + })) ?? []; + + return muteTimingsOptions; + }, [muteTimings]); const formAmRoute = { ...amRouteToFormAmRoute(route), diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index 0560d6bb20b..d90c5307532 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -742,16 +742,18 @@ const TimeIntervals: FC<{ timings: string[]; alertManagerSourceName: string }> = */ return (
- {timings.map((timing) => ( - - {timing} - + {timings.map((timing, index) => ( + + + {timing} + + {index < timings.length - 1 && ', '} + ))}
); diff --git a/public/app/features/alerting/unified/hooks/mergeRequestStates.tsx b/public/app/features/alerting/unified/hooks/mergeRequestStates.tsx new file mode 100644 index 00000000000..f57f6b1d415 --- /dev/null +++ b/public/app/features/alerting/unified/hooks/mergeRequestStates.tsx @@ -0,0 +1,19 @@ +interface RequestState { + error?: unknown; + + isUninitialized: boolean; + isSuccess: boolean; + isError: boolean; + isLoading: boolean; +} + +// @TODO what to do with the other props that we get from RTKQ's state such as originalArgs, etc? +export function mergeRequestStates(...states: RequestState[]): RequestState { + return { + error: states.find((s) => s.error), + isUninitialized: states.every((s) => s.isUninitialized), + isSuccess: states.every((s) => s.isSuccess), + isError: states.some((s) => s.isError), + isLoading: states.some((s) => s.isLoading), + }; +} diff --git a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts deleted file mode 100644 index 777f615edec..00000000000 --- a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts +++ /dev/null @@ -1,27 +0,0 @@ -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'; - -import { useAlertmanagerConfig } from './useAlertmanagerConfig'; - -export function useMuteTimingOptions(): Array> { - const { selectedAlertmanager } = useAlertmanager(); - const { currentData } = useAlertmanagerConfig(selectedAlertmanager); - const config = currentData?.alertmanager_config; - - return useMemo(() => { - const time_intervals = config ? mergeTimeIntervals(config) : []; - const muteTimingsOptions: Array> = - time_intervals?.map((value) => ({ - value: value.name, - label: value.name, - description: value.time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND '), - })) ?? []; - - return muteTimingsOptions; - }, [config]); -} diff --git a/public/app/features/alerting/unified/hooks/useProduceNewAlertmanagerConfig.ts b/public/app/features/alerting/unified/hooks/useProduceNewAlertmanagerConfig.ts index 090591fa486..9a9147473d1 100644 --- a/public/app/features/alerting/unified/hooks/useProduceNewAlertmanagerConfig.ts +++ b/public/app/features/alerting/unified/hooks/useProduceNewAlertmanagerConfig.ts @@ -4,6 +4,7 @@ import reduceReducers from 'reduce-reducers'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { alertmanagerApi } from '../api/alertmanagerApi'; +import { muteTimingsReducer } from '../reducers/alertmanager/muteTimings'; import { useAlertmanager } from '../state/AlertmanagerContext'; import { mergeRequestStates } from './mergeRequestStates'; @@ -24,7 +25,7 @@ export const initialAlertmanagerConfiguration: AlertManagerCortexConfig = { template_files: {}, }; -const configurationReducer = reduceReducers(initialAlertmanagerConfiguration); +const configurationReducer = reduceReducers(initialAlertmanagerConfiguration, muteTimingsReducer); /** * This hook will make sure we are always applying actions that mutate the Alertmanager configuration @@ -52,6 +53,7 @@ export function useProduceNewAlertmanagerConfiguration() { */ const produceNewAlertmanagerConfiguration = async (action: Action) => { const currentAlertmanagerConfiguration = await fetchAlertmanagerConfig(selectedAlertmanager).unwrap(); + const newConfig = configurationReducer(currentAlertmanagerConfiguration, action); return updateAlertManager({ diff --git a/public/app/features/alerting/unified/reducers/alertmanager/__snapshots__/muteTimings.test.ts.snap b/public/app/features/alerting/unified/reducers/alertmanager/__snapshots__/muteTimings.test.ts.snap new file mode 100644 index 00000000000..6baf3442138 --- /dev/null +++ b/public/app/features/alerting/unified/reducers/alertmanager/__snapshots__/muteTimings.test.ts.snap @@ -0,0 +1,329 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`mute timings should be able to add a new mute timing 1`] = ` +{ + "alertmanager_config": { + "mute_time_intervals": [ + { + "name": "default legacy time interval", + "time_intervals": [ + { + "days_of_month": [ + "15", + ], + "months": [ + "august:december", + "march", + ], + "times": [ + { + "end_time": "24:00", + "start_time": "12:00", + }, + ], + }, + ], + }, + ], + "route": { + "routes": [ + { + "mute_time_intervals": [ + "default time interval", + ], + }, + { + "mute_time_intervals": [ + "default legacy time interval", + ], + }, + ], + }, + "time_intervals": [ + { + "name": "default time interval", + "time_intervals": [ + { + "days_of_month": [ + "15", + ], + "months": [ + "august:december", + "march", + ], + "times": [ + { + "end_time": "24:00", + "start_time": "12:00", + }, + ], + }, + ], + }, + { + "name": "new mute time interval", + "time_intervals": [ + { + "days_of_month": [ + "15", + ], + "months": [ + "august:december", + "march", + ], + "times": [ + { + "end_time": "24:00", + "start_time": "12:00", + }, + ], + }, + ], + }, + ], + }, + "template_files": {}, +} +`; + +exports[`mute timings should be able to remove a mute timing 1`] = ` +{ + "alertmanager_config": { + "mute_time_intervals": [], + "route": { + "active_time_intervals": [], + "mute_time_intervals": [], + "routes": [ + { + "active_time_intervals": [], + "mute_time_intervals": [ + "default time interval", + ], + "routes": undefined, + }, + { + "active_time_intervals": [], + "mute_time_intervals": [], + "routes": undefined, + }, + ], + }, + "time_intervals": [ + { + "name": "default time interval", + "time_intervals": [ + { + "days_of_month": [ + "15", + ], + "months": [ + "august:december", + "march", + ], + "times": [ + { + "end_time": "24:00", + "start_time": "12:00", + }, + ], + }, + ], + }, + ], + }, + "template_files": {}, +} +`; + +exports[`mute timings should be able to remove a mute timing 2`] = ` +{ + "alertmanager_config": { + "mute_time_intervals": [ + { + "name": "default legacy time interval", + "time_intervals": [ + { + "days_of_month": [ + "15", + ], + "months": [ + "august:december", + "march", + ], + "times": [ + { + "end_time": "24:00", + "start_time": "12:00", + }, + ], + }, + ], + }, + ], + "route": { + "active_time_intervals": [], + "mute_time_intervals": [], + "routes": [ + { + "active_time_intervals": [], + "mute_time_intervals": [], + "routes": undefined, + }, + { + "active_time_intervals": [], + "mute_time_intervals": [ + "default legacy time interval", + ], + "routes": undefined, + }, + ], + }, + "time_intervals": [], + }, + "template_files": {}, +} +`; + +exports[`mute timings should be able to update a time interval 1`] = ` +{ + "alertmanager_config": { + "mute_time_intervals": [ + { + "name": "new mute time interval", + "time_intervals": [ + { + "days_of_month": [ + "15", + ], + "months": [ + "august:december", + "march", + ], + "times": [ + { + "end_time": "24:00", + "start_time": "12:00", + }, + ], + }, + ], + }, + ], + "route": { + "active_time_intervals": undefined, + "mute_time_intervals": undefined, + "routes": [ + { + "active_time_intervals": undefined, + "mute_time_intervals": [ + "default time interval", + ], + "routes": undefined, + }, + { + "active_time_intervals": undefined, + "mute_time_intervals": [ + "new mute time interval", + ], + "routes": undefined, + }, + ], + }, + "time_intervals": [ + { + "name": "default time interval", + "time_intervals": [ + { + "days_of_month": [ + "15", + ], + "months": [ + "august:december", + "march", + ], + "times": [ + { + "end_time": "24:00", + "start_time": "12:00", + }, + ], + }, + ], + }, + ], + }, + "template_files": {}, +} +`; + +exports[`mute timings should be able to update a time interval 2`] = ` +{ + "alertmanager_config": { + "mute_time_intervals": [ + { + "name": "default legacy time interval", + "time_intervals": [ + { + "days_of_month": [ + "15", + ], + "months": [ + "august:december", + "march", + ], + "times": [ + { + "end_time": "24:00", + "start_time": "12:00", + }, + ], + }, + ], + }, + ], + "route": { + "active_time_intervals": undefined, + "mute_time_intervals": undefined, + "routes": [ + { + "active_time_intervals": undefined, + "mute_time_intervals": [ + "new mute time interval", + ], + "routes": undefined, + }, + { + "active_time_intervals": undefined, + "mute_time_intervals": [ + "default legacy time interval", + ], + "routes": undefined, + }, + ], + }, + "time_intervals": [ + { + "name": "new mute time interval", + "time_intervals": [ + { + "days_of_month": [ + "15", + ], + "months": [ + "august:december", + "march", + ], + "times": [ + { + "end_time": "24:00", + "start_time": "12:00", + }, + ], + }, + ], + }, + ], + }, + "template_files": {}, +} +`; diff --git a/public/app/features/alerting/unified/reducers/alertmanager/muteTimings.test.ts b/public/app/features/alerting/unified/reducers/alertmanager/muteTimings.test.ts new file mode 100644 index 00000000000..d5e7d4a0d0d --- /dev/null +++ b/public/app/features/alerting/unified/reducers/alertmanager/muteTimings.test.ts @@ -0,0 +1,84 @@ +import { UnknownAction } from 'redux'; + +import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; + +import { addMuteTimingAction, deleteMuteTimingAction, muteTimingsReducer, updateMuteTimingAction } from './muteTimings'; + +describe('mute timings', () => { + const initialConfig: AlertManagerCortexConfig = { + alertmanager_config: { + time_intervals: [mockTimeInterval({ name: 'default time interval' })], + mute_time_intervals: [mockTimeInterval({ name: 'default legacy time interval' })], + route: { + routes: [ + { + mute_time_intervals: ['default time interval'], + }, + { + mute_time_intervals: ['default legacy time interval'], + }, + ], + }, + }, + template_files: {}, + }; + + it('should be able to add a new mute timing', () => { + const newMuteTiming = mockTimeInterval({ name: 'new mute time interval' }); + const action = addMuteTimingAction({ interval: newMuteTiming }); + + expect(muteTimingsReducer(initialConfig, action)).toMatchSnapshot(); + }); + + it('should be able to remove a mute timing', () => { + const legacyMuteTimingName = initialConfig.alertmanager_config.mute_time_intervals![0].name; + const deleteFromLegacyKey = deleteMuteTimingAction({ name: legacyMuteTimingName }); + expect(muteTimingsReducer(initialConfig, deleteFromLegacyKey)).toMatchSnapshot(); + + const muteTimingName = initialConfig.alertmanager_config.time_intervals![0].name; + const deleteMuteTiming = deleteMuteTimingAction({ name: muteTimingName }); + expect(muteTimingsReducer(initialConfig, deleteMuteTiming)).toMatchSnapshot(); + }); + + it('should be able to update a time interval', () => { + const newMuteTiming = mockTimeInterval({ name: 'new mute time interval' }); + + const legacyMuteTimingName = initialConfig.alertmanager_config.mute_time_intervals![0].name; + const updateLegacyMuteTiming = updateMuteTimingAction({ + originalName: legacyMuteTimingName, + interval: newMuteTiming, + }); + expect(muteTimingsReducer(initialConfig, updateLegacyMuteTiming)).toMatchSnapshot(); + + const muteTimingName = initialConfig.alertmanager_config.time_intervals![0].name; + const updateMuteTiming = updateMuteTimingAction({ originalName: muteTimingName, interval: newMuteTiming }); + expect(muteTimingsReducer(initialConfig, updateMuteTiming)).toMatchSnapshot(); + }); + + it('should throw for unknown action', () => { + const action: UnknownAction = { type: 'unknown' }; + + expect(() => { + muteTimingsReducer(initialConfig, action); + }).toThrow('unknown'); + }); +}); + +function mockTimeInterval(overrides: Partial = {}): MuteTimeInterval { + return { + name: 'mock time interval', + time_intervals: [ + { + times: [ + { + start_time: '12:00', + end_time: '24:00', + }, + ], + days_of_month: ['15'], + months: ['august:december', 'march'], + }, + ], + ...overrides, + }; +} diff --git a/public/app/features/alerting/unified/reducers/alertmanager/muteTimings.ts b/public/app/features/alerting/unified/reducers/alertmanager/muteTimings.ts new file mode 100644 index 00000000000..1427452bdde --- /dev/null +++ b/public/app/features/alerting/unified/reducers/alertmanager/muteTimings.ts @@ -0,0 +1,75 @@ +import { createAction, createReducer } from '@reduxjs/toolkit'; +import { remove } from 'lodash'; + +import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; + +import { removeTimeIntervalFromRoute, renameTimeInterval } from '../../utils/alertmanager'; + +export const addMuteTimingAction = createAction<{ interval: MuteTimeInterval }>('muteTiming/add'); +export const updateMuteTimingAction = createAction<{ + interval: MuteTimeInterval; + originalName: string; +}>('muteTiming/update'); +export const deleteMuteTimingAction = createAction<{ name: string }>('muteTiming/delete'); + +const initialState: AlertManagerCortexConfig = { + alertmanager_config: {}, + template_files: {}, +}; + +/** + * This reducer will manage action related to mute timings and make sure all operations on the alertmanager + * configuration happen immutably and only mutate what they need. + */ +export const muteTimingsReducer = createReducer(initialState, (builder) => { + builder + // add a mute timing to the alertmanager configuration + .addCase(addMuteTimingAction, (draft, { payload }) => { + const { interval } = payload; + draft.alertmanager_config.time_intervals = (draft.alertmanager_config.time_intervals ?? []).concat(interval); + }) + // add a mute timing to the alertmanager configuration + // make sure we update the mute timing in either the deprecated or the new time intervals property + .addCase(updateMuteTimingAction, (draft, { payload }) => { + const { interval, originalName } = payload; + const nameHasChanged = interval.name !== originalName; + + const timeIntervals = draft.alertmanager_config.time_intervals ?? []; + const muteTimeIntervals = draft.alertmanager_config.mute_time_intervals ?? []; + + const existingIntervalIndex = timeIntervals.findIndex(({ name }) => name === originalName); + if (existingIntervalIndex !== -1) { + timeIntervals[existingIntervalIndex] = interval; + } + + const existingMuteIntervalIndex = muteTimeIntervals.findIndex(({ name }) => name === originalName); + if (existingMuteIntervalIndex !== -1) { + muteTimeIntervals[existingMuteIntervalIndex] = interval; + } + + if (nameHasChanged && draft.alertmanager_config.route) { + draft.alertmanager_config.route = renameTimeInterval( + interval.name, + originalName, + draft.alertmanager_config.route + ); + } + }) + // delete a mute timing from the alertmanager configuration, since the configuration might be using the "deprecated" mute_time_intervals + // let's also check there + .addCase(deleteMuteTimingAction, (draft, { payload }) => { + const { name } = payload; + const { alertmanager_config } = draft; + const { time_intervals = [], mute_time_intervals = [] } = alertmanager_config; + + // remove the mute timings from the legacy and new time intervals definition + remove(time_intervals, (interval) => interval.name === name); + remove(mute_time_intervals, (interval) => interval.name === name); + + // remove the mute timing from all routes + alertmanager_config.route = removeTimeIntervalFromRoute(name, alertmanager_config.route ?? {}); + }) + .addDefaultCase((_state, action) => { + throw new Error(`Unknown action for mute timing reducer: ${action.type}`); + }); +}); diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 018a3ff68ff..8f9b4b9fcb0 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -52,7 +52,7 @@ import { discoverFeatures } from '../api/buildInfo'; import { FetchPromRulesFilter, fetchRules } from '../api/prometheus'; import { FetchRulerRulesFilter, deleteRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from '../api/ruler'; import { RuleFormValues } from '../types/rule-form'; -import { addDefaultsToAlertmanagerConfig, removeTimeIntervalFromRoute } from '../utils/alertmanager'; +import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager'; import { GRAFANA_RULES_SOURCE_NAME, getAllRulesSourceNames, @@ -557,54 +557,6 @@ export const deleteAlertManagerConfigAction = createAsyncThunk( } ); -export const deleteMuteTimingAction = (alertManagerSourceName: string, muteTimingName: string): ThunkResult => { - return async (dispatch) => { - const config = await dispatch( - alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName) - ).unwrap(); - - 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, - }; - - const { mute_time_intervals: _, ...configWithoutMuteTimings } = config?.alertmanager_config ?? {}; - return withAppEvents( - dispatch( - updateAlertManagerConfigAction({ - alertManagerSourceName, - oldConfig: config, - newConfig: { - ...config, - alertmanager_config: { - ...configWithoutMuteTimings, - route: config.alertmanager_config.route - ? removeTimeIntervalFromRoute(muteTimingName, config.alertmanager_config?.route) - : undefined, - ...time_intervals_without_mute_to_save, - }, - }, - }) - ), - { - successMessage: `Deleted "${muteTimingName}" from Alertmanager configuration`, - errorMessage: 'Failed to delete mute timing', - } - ); - }; -}; - interface TestReceiversOptions { alertManagerSourceName: string; receivers: Receiver[];