Alerting: use new Alertmanager update hooks for mute and active time intervals (#91404)

pull/91850/head
Gilles De Mey 11 months ago committed by GitHub
parent 7b919e3277
commit 0a2db346ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      public/app/features/alerting/unified/MuteTimings.test.tsx
  2. 27
      public/app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons.tsx
  3. 12
      public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx
  4. 5
      public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx
  5. 144
      public/app/features/alerting/unified/components/mute-timings/useMuteTimings.tsx
  6. 23
      public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx
  7. 22
      public/app/features/alerting/unified/components/notification-policies/Policy.tsx
  8. 19
      public/app/features/alerting/unified/hooks/mergeRequestStates.tsx
  9. 27
      public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts
  10. 4
      public/app/features/alerting/unified/hooks/useProduceNewAlertmanagerConfig.ts
  11. 329
      public/app/features/alerting/unified/reducers/alertmanager/__snapshots__/muteTimings.test.ts.snap
  12. 84
      public/app/features/alerting/unified/reducers/alertmanager/muteTimings.test.ts
  13. 75
      public/app/features/alerting/unified/reducers/alertmanager/muteTimings.ts
  14. 50
      public/app/features/alerting/unified/state/actions.ts

@ -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<AlertManagerCortexConfig> => {
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],

@ -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 = (
<LinkButton href={viewOrEditHref} variant="secondary" size="sm" icon={muteTiming.provisioned ? 'eye' : 'pen'}>
<LinkButton
href={viewOrEditHref}
variant="secondary"
size="sm"
icon={muteTiming.provisioned ? 'eye' : 'pen'}
disabled={isLoading(deleteMuteTimingRequestState)}
>
{muteTiming.provisioned ? (
<Trans i18nKey="alerting.common.view">View</Trans>
) : (
@ -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)}
>
<Trans i18nKey="alerting.common.export">Export</Trans>
@ -61,7 +70,13 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }:
{!muteTiming.provisioned && (
<Authorize actions={[AlertmanagerAction.DeleteMuteTiming]}>
<LinkButton icon="trash-alt" variant="secondary" size="sm" onClick={() => setShowDeleteDrawer(true)}>
<LinkButton
icon="trash-alt"
variant="secondary"
size="sm"
onClick={() => setShowDeleteDrawer(true)}
disabled={isLoading(deleteMuteTimingRequestState)}
>
<Trans i18nKey="alerting.common.delete">Delete</Trans>
</LinkButton>
</Authorize>
@ -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();

@ -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(() => {

@ -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);
});

@ -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) => {

@ -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<SelectableValue<string>> =
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),

@ -742,16 +742,18 @@ const TimeIntervals: FC<{ timings: string[]; alertManagerSourceName: string }> =
*/
return (
<div>
{timings.map((timing) => (
<TextLink
key={timing}
href={createMuteTimingLink(timing, alertManagerSourceName)}
color="primary"
variant="bodySmall"
inline={false}
>
{timing}
</TextLink>
{timings.map((timing, index) => (
<Fragment key={timing}>
<TextLink
href={createMuteTimingLink(timing, alertManagerSourceName)}
color="primary"
variant="bodySmall"
inline={false}
>
{timing}
</TextLink>
{index < timings.length - 1 && ', '}
</Fragment>
))}
</div>
);

@ -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),
};
}

@ -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<SelectableValue<string>> {
const { selectedAlertmanager } = useAlertmanager();
const { currentData } = useAlertmanagerConfig(selectedAlertmanager);
const config = currentData?.alertmanager_config;
return useMemo(() => {
const time_intervals = config ? mergeTimeIntervals(config) : [];
const muteTimingsOptions: Array<SelectableValue<string>> =
time_intervals?.map((value) => ({
value: value.name,
label: value.name,
description: value.time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND '),
})) ?? [];
return muteTimingsOptions;
}, [config]);
}

@ -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({

@ -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": {},
}
`;

@ -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> = {}): 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,
};
}

@ -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}`);
});
});

@ -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<void> => {
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[];

Loading…
Cancel
Save