Alerting: implement deleting templates & receivers (#33677)

This commit is contained in:
Domas 2021-05-06 14:51:44 +03:00 committed by GitHub
parent 10a4606315
commit b5b8877253
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 182 additions and 16 deletions

View File

@ -57,13 +57,18 @@ const Silences: FC = () => {
{error.message || 'Unknown error.'} {error.message || 'Unknown error.'}
</Alert> </Alert>
)} )}
{alertsRequest?.error && !alertsRequest?.loading && (
<Alert severity="error" title="Error loading alert manager alerts">
{alertsRequest.error?.message || 'Unknown error.'}
</Alert>
)}
{loading && <LoadingPlaceholder text="loading silences..." />} {loading && <LoadingPlaceholder text="loading silences..." />}
{result && !error && alertsRequest?.result && ( {result && !error && (
<Switch> <Switch>
<Route exact path="/alerting/silences"> <Route exact path="/alerting/silences">
<SilencesTable <SilencesTable
silences={result} silences={result}
alertManagerAlerts={alertsRequest.result} alertManagerAlerts={alertsRequest?.result ?? []}
alertManagerSourceName={alertManagerSourceName} alertManagerSourceName={alertManagerSourceName}
/> />
</Route> </Route>

View File

@ -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 { 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 { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { getAlertTableStyles } from '../../styles/table'; import { getAlertTableStyles } from '../../styles/table';
import { extractReadableNotifierTypes } from '../../utils/receivers'; import { extractReadableNotifierTypes } from '../../utils/receivers';
@ -9,6 +9,9 @@ import { ReceiversSection } from './ReceiversSection';
import { makeAMLink } from '../../utils/misc'; import { makeAMLink } from '../../utils/misc';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { isReceiverUsed } from '../../utils/alertmanager-config';
import { useDispatch } from 'react-redux';
import { deleteReceiverAction } from '../../state/actions';
interface Props { interface Props {
config: AlertManagerCortexConfig; config: AlertManagerCortexConfig;
@ -16,11 +19,31 @@ interface Props {
} }
export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => { export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
const dispatch = useDispatch();
const tableStyles = useStyles2(getAlertTableStyles); const tableStyles = useStyles2(getAlertTableStyles);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers); 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<string>();
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( const rows = useMemo(
() => () =>
config.alertmanager_config.receivers?.map((receiver) => ({ config.alertmanager_config.receivers?.map((receiver) => ({
@ -71,12 +94,43 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
tooltip="Edit contact point" tooltip="Edit contact point"
icon="pen" icon="pen"
/> />
<ActionIcon tooltip="Delete contact point" icon="trash-alt" /> <ActionIcon
onClick={() => onClickDeleteReceiver(receiver.name)}
tooltip="Delete contact point"
icon="trash-alt"
/>
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
{!!showCannotDeleteReceiverModal && (
<Modal
isOpen={true}
title="Cannot delete contact point"
onDismiss={() => setShowCannotDeleteReceiverModal(false)}
>
<p>
Contact point cannot be deleted because it is used in more policies. Please update or delete these policies
first.
</p>
<Modal.ButtonRow>
<Button variant="secondary" onClick={() => setShowCannotDeleteReceiverModal(false)} fill="outline">
Close
</Button>
</Modal.ButtonRow>
</Modal>
)}
{!!receiverToDelete && (
<ConfirmModal
isOpen={true}
title="Delete contact point"
body={`Are you sure you want to delete contact point "${receiverToDelete}"?`}
confirmText="Yes, delete"
onConfirm={deleteReceiver}
onDismiss={() => setReceiverToDelete(undefined)}
/>
)}
</ReceiversSection> </ReceiversSection>
); );
}; };

View File

@ -1,4 +1,4 @@
import { useStyles2 } from '@grafana/ui'; import { ConfirmModal, useStyles2 } from '@grafana/ui';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import React, { FC, Fragment, useMemo, useState } from 'react'; import React, { FC, Fragment, useMemo, useState } from 'react';
import { getAlertTableStyles } from '../../styles/table'; import { getAlertTableStyles } from '../../styles/table';
@ -7,6 +7,8 @@ import { DetailsField } from '../DetailsField';
import { ActionIcon } from '../rules/ActionIcon'; import { ActionIcon } from '../rules/ActionIcon';
import { ReceiversSection } from './ReceiversSection'; import { ReceiversSection } from './ReceiversSection';
import { makeAMLink } from '../../utils/misc'; import { makeAMLink } from '../../utils/misc';
import { useDispatch } from 'react-redux';
import { deleteTemplateAction } from '../../state/actions';
interface Props { interface Props {
config: AlertManagerCortexConfig; config: AlertManagerCortexConfig;
@ -14,10 +16,19 @@ interface Props {
} }
export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => { export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
const dispatch = useDispatch();
const [expandedTemplates, setExpandedTemplates] = useState<Record<string, boolean>>({}); const [expandedTemplates, setExpandedTemplates] = useState<Record<string, boolean>>({});
const tableStyles = useStyles2(getAlertTableStyles); const tableStyles = useStyles2(getAlertTableStyles);
const templateRows = useMemo(() => Object.entries(config.template_files), [config]); const templateRows = useMemo(() => Object.entries(config.template_files), [config]);
const [templateToDelete, setTemplateToDelete] = useState<string>();
const deleteTemplate = () => {
if (templateToDelete) {
dispatch(deleteTemplateAction(templateToDelete, alertManagerName));
}
setTemplateToDelete(undefined);
};
return ( return (
<ReceiversSection <ReceiversSection
@ -66,7 +77,7 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
tooltip="edit template" tooltip="edit template"
icon="pen" icon="pen"
/> />
<ActionIcon tooltip="delete template" icon="trash-alt" /> <ActionIcon onClick={() => setTemplateToDelete(name)} tooltip="delete template" icon="trash-alt" />
</td> </td>
</tr> </tr>
{isExpanded && ( {isExpanded && (
@ -84,6 +95,17 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
})} })}
</tbody> </tbody>
</table> </table>
{!!templateToDelete && (
<ConfirmModal
isOpen={true}
title="Delete template"
body={`Are you sure you want to delete template "${templateToDelete}"?`}
confirmText="Yes, delete"
onConfirm={deleteTemplate}
onDismiss={() => setTemplateToDelete(undefined)}
/>
)}
</ReceiversSection> </ReceiversSection>
); );
}; };

View File

@ -31,6 +31,7 @@ const getStyles = (theme: GrafanaTheme) => ({
parent: css` parent: css`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
max-width: ${theme.breakpoints.xl};
`, `,
description: css` description: css`
margin-top: -${theme.spacing.md}; margin-top: -${theme.spacing.md};

View File

@ -145,7 +145,7 @@ export const RulesTable: FC<Props> = ({
<ActionIcon <ActionIcon
icon="pen" icon="pen"
tooltip="edit rule" tooltip="edit rule"
to={`alerting/${encodeURIComponent( to={`/alerting/${encodeURIComponent(
stringifyRuleIdentifier( stringifyRuleIdentifier(
getRuleIdentifier(getRulesSourceName(rulesSource), namespace.name, group.name, rulerRule) getRuleIdentifier(getRulesSourceName(rulesSource), namespace.name, group.name, rulerRule)
) )

View File

@ -13,6 +13,7 @@ import { SilencePeriod } from './SilencePeriod';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { makeAMLink } from '../../utils/misc'; import { makeAMLink } from '../../utils/misc';
import { useCleanup } from 'app/core/hooks/useCleanup';
interface Props { interface Props {
silence?: Silence; silence?: Silence;
@ -58,6 +59,8 @@ export const SilencesEditor: FC<Props> = ({ silence, alertManagerSourceName }) =
const { loading, error } = useUnifiedAlertingSelector((state) => state.updateSilence); const { loading, error } = useUnifiedAlertingSelector((state) => state.updateSilence);
useCleanup((state) => state.unifiedAlerting.updateSilence);
const { register, handleSubmit, formState } = formAPI; const { register, handleSubmit, formState } = formAPI;
const onSubmit = (data: SilenceFormFields) => { const onSubmit = (data: SilenceFormFields) => {

View File

@ -25,11 +25,13 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
<> <>
{!!silences.length && ( {!!silences.length && (
<> <>
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}> <div className={styles.topButtonContainer}>
<Button className={styles.addNewSilence} icon="plus"> <Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>
New Silence <Button className={styles.addNewSilence} icon="plus">
</Button> New Silence
</Link> </Button>
</Link>
</div>
<table className={tableStyles.table}> <table className={tableStyles.table}>
<colgroup> <colgroup>
<col className={tableStyles.colExpand} /> <col className={tableStyles.colExpand} />
@ -76,6 +78,11 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
}; };
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
topButtonContainer: css`
display: flex;
flex-direction: row;
justify-content: flex-end;
`,
addNewSilence: css` addNewSilence: css`
margin-bottom: ${theme.spacing(1)}; margin-bottom: ${theme.spacing(1)};
`, `,

View File

@ -337,11 +337,12 @@ interface UpdateAlertManagerConfigActionOptions {
newConfig: AlertManagerCortexConfig; newConfig: AlertManagerCortexConfig;
successMessage?: string; // show toast on success successMessage?: string; // show toast on success
redirectPath?: string; // where to redirect on success redirectPath?: string; // where to redirect on success
refetch?: boolean; // refetch config on success
} }
export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlertManagerConfigActionOptions, {}>( export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlertManagerConfigActionOptions, {}>(
'unifiedalerting/updateAMConfig', 'unifiedalerting/updateAMConfig',
({ alertManagerSourceName, oldConfig, newConfig, successMessage, redirectPath }): Promise<void> => ({ alertManagerSourceName, oldConfig, newConfig, successMessage, redirectPath, refetch }, thunkAPI): Promise<void> =>
withSerializedError( withSerializedError(
(async () => { (async () => {
const latestConfig = await fetchAlertManagerConfig(alertManagerSourceName); const latestConfig = await fetchAlertManagerConfig(alertManagerSourceName);
@ -350,11 +351,13 @@ export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlert
'It seems configuration has been recently updated. Please reload page and try again to make sure that recent changes are not overwritten.' 'It seems configuration has been recently updated. Please reload page and try again to make sure that recent changes are not overwritten.'
); );
} }
await updateAlertManagerConfig(alertManagerSourceName, addDefaultsToAlertmanagerConfig(newConfig)); await updateAlertManagerConfig(alertManagerSourceName, addDefaultsToAlertmanagerConfig(newConfig));
if (successMessage) { if (successMessage) {
appEvents?.emit(AppEvents.alertSuccess, [successMessage]); appEvents?.emit(AppEvents.alertSuccess, [successMessage]);
} }
if (refetch) {
await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
}
if (redirectPath) { if (redirectPath) {
locationService.push(makeAMLink(redirectPath, alertManagerSourceName)); locationService.push(makeAMLink(redirectPath, alertManagerSourceName));
} }
@ -398,3 +401,62 @@ export const createOrUpdateSilenceAction = createAsyncThunk<void, UpdateSilenceA
})() })()
) )
); );
export const deleteReceiverAction = (receiverName: string, alertManagerSourceName: string): ThunkResult<void> => {
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<void> => {
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,
})
);
};
};

View File

@ -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 { export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
// add default receiver if it does not exist // add default receiver if it does not exist
@ -16,3 +16,15 @@ export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig
} }
return config; 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
);
}