mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: implement deleting templates & receivers (#33677)
This commit is contained in:
parent
10a4606315
commit
b5b8877253
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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};
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
|
@ -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) => {
|
||||||
|
@ -25,11 +25,13 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
|||||||
<>
|
<>
|
||||||
{!!silences.length && (
|
{!!silences.length && (
|
||||||
<>
|
<>
|
||||||
|
<div className={styles.topButtonContainer}>
|
||||||
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>
|
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>
|
||||||
<Button className={styles.addNewSilence} icon="plus">
|
<Button className={styles.addNewSilence} icon="plus">
|
||||||
New Silence
|
New Silence
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</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)};
|
||||||
`,
|
`,
|
||||||
|
@ -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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user