From b5b887725397b729308fc16c98e92a6f33094700 Mon Sep 17 00:00:00 2001 From: Domas Date: Thu, 6 May 2021 14:51:44 +0300 Subject: [PATCH] Alerting: implement deleting templates & receivers (#33677) --- .../features/alerting/unified/Silences.tsx | 9 ++- .../components/receivers/ReceiversTable.tsx | 60 ++++++++++++++++- .../components/receivers/TemplatesTable.tsx | 26 +++++++- .../rule-editor/RuleEditorSection.tsx | 1 + .../unified/components/rules/RulesTable.tsx | 2 +- .../components/silences/SilencesEditor.tsx | 3 + .../components/silences/SilencesTable.tsx | 17 +++-- .../alerting/unified/state/actions.ts | 66 ++++++++++++++++++- .../unified/utils/alertmanager-config.ts | 14 +++- 9 files changed, 182 insertions(+), 16 deletions(-) diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index 240ed57f856..f0c0449e089 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -57,13 +57,18 @@ const Silences: FC = () => { {error.message || 'Unknown error.'} )} + {alertsRequest?.error && !alertsRequest?.loading && ( + + {alertsRequest.error?.message || 'Unknown error.'} + + )} {loading && } - {result && !error && alertsRequest?.result && ( + {result && !error && ( diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx index b5e5b855440..564b5c69511 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx @@ -1,6 +1,6 @@ -import { useStyles2 } from '@grafana/ui'; +import { Button, ConfirmModal, Modal, useStyles2 } from '@grafana/ui'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; -import React, { FC, useMemo } from 'react'; +import React, { FC, useMemo, useState } from 'react'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { getAlertTableStyles } from '../../styles/table'; import { extractReadableNotifierTypes } from '../../utils/receivers'; @@ -9,6 +9,9 @@ import { ReceiversSection } from './ReceiversSection'; import { makeAMLink } from '../../utils/misc'; import { GrafanaTheme2 } from '@grafana/data'; import { css } from '@emotion/css'; +import { isReceiverUsed } from '../../utils/alertmanager-config'; +import { useDispatch } from 'react-redux'; +import { deleteReceiverAction } from '../../state/actions'; interface Props { config: AlertManagerCortexConfig; @@ -16,11 +19,31 @@ interface Props { } export const ReceiversTable: FC = ({ config, alertManagerName }) => { + const dispatch = useDispatch(); const tableStyles = useStyles2(getAlertTableStyles); const styles = useStyles2(getStyles); const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers); + // receiver name slated for deletion. If this is set, a confirmation modal is shown. If user approves, this receiver is deleted + const [receiverToDelete, setReceiverToDelete] = useState(); + const [showCannotDeleteReceiverModal, setShowCannotDeleteReceiverModal] = useState(false); + + const onClickDeleteReceiver = (receiverName: string): void => { + if (isReceiverUsed(receiverName, config)) { + setShowCannotDeleteReceiverModal(true); + } else { + setReceiverToDelete(receiverName); + } + }; + + const deleteReceiver = () => { + if (receiverToDelete) { + dispatch(deleteReceiverAction(receiverToDelete, alertManagerName)); + } + setReceiverToDelete(undefined); + }; + const rows = useMemo( () => config.alertmanager_config.receivers?.map((receiver) => ({ @@ -71,12 +94,43 @@ export const ReceiversTable: FC = ({ config, alertManagerName }) => { tooltip="Edit contact point" icon="pen" /> - + onClickDeleteReceiver(receiver.name)} + tooltip="Delete contact point" + icon="trash-alt" + /> ))} + {!!showCannotDeleteReceiverModal && ( + setShowCannotDeleteReceiverModal(false)} + > +

+ Contact point cannot be deleted because it is used in more policies. Please update or delete these policies + first. +

+ + + +
+ )} + {!!receiverToDelete && ( + setReceiverToDelete(undefined)} + /> + )} ); }; diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx index a941dd854a6..48f9d83b7a3 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx @@ -1,4 +1,4 @@ -import { useStyles2 } from '@grafana/ui'; +import { ConfirmModal, useStyles2 } from '@grafana/ui'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import React, { FC, Fragment, useMemo, useState } from 'react'; import { getAlertTableStyles } from '../../styles/table'; @@ -7,6 +7,8 @@ import { DetailsField } from '../DetailsField'; import { ActionIcon } from '../rules/ActionIcon'; import { ReceiversSection } from './ReceiversSection'; import { makeAMLink } from '../../utils/misc'; +import { useDispatch } from 'react-redux'; +import { deleteTemplateAction } from '../../state/actions'; interface Props { config: AlertManagerCortexConfig; @@ -14,10 +16,19 @@ interface Props { } export const TemplatesTable: FC = ({ config, alertManagerName }) => { + const dispatch = useDispatch(); const [expandedTemplates, setExpandedTemplates] = useState>({}); const tableStyles = useStyles2(getAlertTableStyles); const templateRows = useMemo(() => Object.entries(config.template_files), [config]); + const [templateToDelete, setTemplateToDelete] = useState(); + + const deleteTemplate = () => { + if (templateToDelete) { + dispatch(deleteTemplateAction(templateToDelete, alertManagerName)); + } + setTemplateToDelete(undefined); + }; return ( = ({ config, alertManagerName }) => { tooltip="edit template" icon="pen" /> - + setTemplateToDelete(name)} tooltip="delete template" icon="trash-alt" /> {isExpanded && ( @@ -84,6 +95,17 @@ export const TemplatesTable: FC = ({ config, alertManagerName }) => { })} + + {!!templateToDelete && ( + setTemplateToDelete(undefined)} + /> + )} ); }; diff --git a/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx b/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx index 248a145ec1a..d6038884b1b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx @@ -31,6 +31,7 @@ const getStyles = (theme: GrafanaTheme) => ({ parent: css` display: flex; flex-direction: row; + max-width: ${theme.breakpoints.xl}; `, description: css` margin-top: -${theme.spacing.md}; diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index 741733e3cc7..8f89c103fec 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -145,7 +145,7 @@ export const RulesTable: FC = ({ = ({ silence, alertManagerSourceName }) = const { loading, error } = useUnifiedAlertingSelector((state) => state.updateSilence); + useCleanup((state) => state.unifiedAlerting.updateSilence); + const { register, handleSubmit, formState } = formAPI; const onSubmit = (data: SilenceFormFields) => { diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index c63a2ea88c7..e32d6c20b78 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -25,11 +25,13 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo <> {!!silences.length && ( <> - - - +
+ + + +
@@ -76,6 +78,11 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo }; const getStyles = (theme: GrafanaTheme2) => ({ + topButtonContainer: css` + display: flex; + flex-direction: row; + justify-content: flex-end; + `, addNewSilence: css` margin-bottom: ${theme.spacing(1)}; `, diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 6721ad779f6..03286a6b405 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -337,11 +337,12 @@ interface UpdateAlertManagerConfigActionOptions { newConfig: AlertManagerCortexConfig; successMessage?: string; // show toast on success redirectPath?: string; // where to redirect on success + refetch?: boolean; // refetch config on success } export const updateAlertManagerConfigAction = createAsyncThunk( 'unifiedalerting/updateAMConfig', - ({ alertManagerSourceName, oldConfig, newConfig, successMessage, redirectPath }): Promise => + ({ alertManagerSourceName, oldConfig, newConfig, successMessage, redirectPath, refetch }, thunkAPI): Promise => withSerializedError( (async () => { const latestConfig = await fetchAlertManagerConfig(alertManagerSourceName); @@ -350,11 +351,13 @@ export const updateAlertManagerConfigAction = createAsyncThunk => { + return (dispatch, getState) => { + const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result; + if (!config) { + throw new Error(`Config for ${alertManagerSourceName} not found`); + } + if (!config.alertmanager_config.receivers?.find((receiver) => receiver.name === receiverName)) { + throw new Error(`Cannot delete receiver ${receiverName}: not found in config.`); + } + const newConfig: AlertManagerCortexConfig = { + ...config, + alertmanager_config: { + ...config.alertmanager_config, + receivers: config.alertmanager_config.receivers.filter((receiver) => receiver.name !== receiverName), + }, + }; + return dispatch( + updateAlertManagerConfigAction({ + newConfig, + oldConfig: config, + alertManagerSourceName, + successMessage: 'Contact point deleted.', + refetch: true, + }) + ); + }; +}; + +export const deleteTemplateAction = (templateName: string, alertManagerSourceName: string): ThunkResult => { + return (dispatch, getState) => { + const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result; + if (!config) { + throw new Error(`Config for ${alertManagerSourceName} not found`); + } + if (typeof config.template_files?.[templateName] !== 'string') { + throw new Error(`Cannot delete template ${templateName}: not found in config.`); + } + const newTemplates = { ...config.template_files }; + delete newTemplates[templateName]; + const newConfig: AlertManagerCortexConfig = { + ...config, + alertmanager_config: { + ...config.alertmanager_config, + templates: config.alertmanager_config.templates?.filter((existing) => existing !== templateName), + }, + template_files: newTemplates, + }; + return dispatch( + updateAlertManagerConfigAction({ + newConfig, + oldConfig: config, + alertManagerSourceName, + successMessage: 'Template deleted.', + refetch: true, + }) + ); + }; +}; diff --git a/public/app/features/alerting/unified/utils/alertmanager-config.ts b/public/app/features/alerting/unified/utils/alertmanager-config.ts index b6869d9392b..289d8187acd 100644 --- a/public/app/features/alerting/unified/utils/alertmanager-config.ts +++ b/public/app/features/alerting/unified/utils/alertmanager-config.ts @@ -1,4 +1,4 @@ -import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; +import { AlertManagerCortexConfig, Route } from 'app/plugins/datasource/alertmanager/types'; export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig { // add default receiver if it does not exist @@ -16,3 +16,15 @@ export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig } return config; } + +function isReceiverUsedInRoute(receiver: string, route: Route): boolean { + return ( + (route.receiver === receiver || route.routes?.some((route) => isReceiverUsedInRoute(receiver, route))) ?? false + ); +} + +export function isReceiverUsed(receiver: string, config: AlertManagerCortexConfig): boolean { + return ( + (config.alertmanager_config.route && isReceiverUsedInRoute(receiver, config.alertmanager_config.route)) ?? false + ); +}