Alerting: Add useNotificationTemplates hook to abstract away templates loading (#91468)

* Add useNotificationTemplates hook to abstract away templates loading

* Add useUpdateNotificationTemplate hook to abstract away updating logic

* Add useDeleteNotificationTemplate hook to abstract away deletiong logic

* Fix and update templatestable tests

* Remove old code

* Improve error handling

* Remove obsolete test

* Fix and improve tests

* Adjust code style

* Update test snapshot, remove redirects in hooks

* Remove unused code, add provenance none handling, fix redirect url

* Improve provisioning state handling
This commit is contained in:
Konrad Lalik
2024-08-07 11:50:07 +02:00
committed by GitHub
parent cf5e49dbb8
commit 9c5a91009b
10 changed files with 298 additions and 224 deletions
@@ -1,9 +1,7 @@
import * as React from 'react';
import { render, screen, userEvent } from 'test/test-utils';
import { render, screen } from 'test/test-utils';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { setGrafanaAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/configure';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import Templates from './Templates';
@@ -23,35 +21,10 @@ describe('Templates routes', () => {
it('allows duplication of template with spaces in name', async () => {
render(<Templates />, {
historyOptions: {
initialEntries: ['/alerting/notifications/templates/some%20template/duplicate?alertmanager=grafana'],
initialEntries: ['/alerting/notifications/templates/template%20with%20spaces/duplicate?alertmanager=grafana'],
},
});
expect(await screen.findByText('Edit payload')).toBeInTheDocument();
});
it('shows an error when remote AM config has been updated ', async () => {
const originalConfig: AlertManagerCortexConfig = {
template_files: {},
alertmanager_config: {},
};
setGrafanaAlertmanagerConfig(originalConfig);
const user = userEvent.setup();
render(<Templates />, {
historyOptions: {
initialEntries: ['/alerting/notifications/templates/new'],
},
});
await user.type(await screen.findByLabelText(/template name/i), 'a');
// Once the user has loaded the page and started creating their template,
// update the API behaviour as if another user has also edited the config and added something in
setGrafanaAlertmanagerConfig({ ...originalConfig, template_files: { a: 'b' } });
await user.click(screen.getByRole('button', { name: /save/i }));
expect(await screen.findByText(/a newer alertmanager configuration is available/i)).toBeInTheDocument();
});
});
@@ -0,0 +1,73 @@
import { render, screen, within } from 'test/test-utils';
import { AccessControlAction } from 'app/types';
import { setupMswServer } from '../../mockApi';
import { grantUserPermissions } from '../../mocks';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { NotificationTemplates } from './NotificationTemplates';
const renderWithProvider = () => {
render(
<AlertmanagerProvider accessType={'notification'}>
<NotificationTemplates />
</AlertmanagerProvider>
);
};
setupMswServer();
describe('NotificationTemplates', () => {
beforeEach(() => {
jest.resetAllMocks();
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsWrite,
AccessControlAction.AlertingNotificationsExternalRead,
AccessControlAction.AlertingNotificationsExternalWrite,
]);
});
it('Should render templates table with the correct rows', async () => {
renderWithProvider();
const slackRow = await screen.findByRole('row', { name: /slack-template/i });
expect(within(slackRow).getByRole('cell', { name: /slack-template/i })).toBeInTheDocument();
const emailRow = await screen.findByRole('row', { name: /custom-email/i });
expect(within(emailRow).getByRole('cell', { name: /custom-email/i })).toBeInTheDocument();
const provisionedRow = await screen.findByRole('row', { name: /provisioned-template/i });
expect(within(provisionedRow).getByRole('cell', { name: /provisioned-template/i })).toBeInTheDocument();
});
it('Should render duplicate template button when having permissions', async () => {
renderWithProvider();
const slackRow = await screen.findByRole('row', { name: /slack-template/i });
expect(within(slackRow).getByRole('cell', { name: /Copy/i })).toBeInTheDocument();
});
it('Should not render duplicate template button when not having write permissions', async () => {
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsExternalRead,
]);
renderWithProvider();
const slackRow = await screen.findByRole('row', { name: /slack-template/i });
expect(within(slackRow).queryByRole('cell', { name: /Copy/i })).not.toBeInTheDocument();
const emailRow = await screen.findByRole('row', { name: /custom-email/i });
expect(within(emailRow).queryByRole('cell', { name: /Copy/i })).not.toBeInTheDocument();
});
it('shows provisioned badge appropriately', async () => {
renderWithProvider();
const provisionedRow = await screen.findByRole('row', { name: /provisioned-template/i });
expect(within(provisionedRow).getByText('Provisioned')).toBeInTheDocument();
});
});
@@ -1,19 +1,25 @@
import { Alert } from '@grafana/ui';
import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { stringifyErrorLike } from '../../utils/misc';
import { TemplatesTable } from '../receivers/TemplatesTable';
import { useNotificationTemplates } from './useNotificationTemplates';
export const NotificationTemplates = () => {
const { selectedAlertmanager } = useAlertmanager();
const { data, error } = useAlertmanagerConfig(selectedAlertmanager);
const { data: templates, isLoading, error } = useNotificationTemplates({ alertmanager: selectedAlertmanager ?? '' });
if (error) {
return <Alert title="Failed to fetch notification templates">{String(error)}</Alert>;
return <Alert title="Failed to fetch notification templates">{stringifyErrorLike(error)}</Alert>;
}
if (data) {
return <TemplatesTable config={data} alertManagerName={selectedAlertmanager!} />;
if (isLoading) {
return <LoadingPlaceholder text="Loading notification templates" />;
}
if (templates) {
return <TemplatesTable alertManagerName={selectedAlertmanager!} templates={templates} />;
}
return null;
@@ -1,6 +1,12 @@
{
"template_files": {
"some template": "{{ define 'some template' }} something {{ end }}"
"slack-template": "{{ define 'slack-template' }} Custom slack template {{ end }}",
"custom-email": "{{ define 'custom-email' }} Custom email template {{ end }}",
"provisioned-template": "{{ define 'provisioned-template' }} Custom provisioned template {{ end }}",
"template with spaces": "{{ define 'template with spaces' }} Custom template with spaces in the name {{ end }}"
},
"template_file_provenances": {
"provisioned-template": "api"
},
"alertmanager_config": {
"route": {
@@ -89,6 +95,7 @@
}
]
}
]
],
"templates": ["slack-template", "custom-email", "provisioned-template", "template with spaces"]
}
}
@@ -152,10 +152,21 @@ exports[`should be able to test and save a receiver 2`] = `
},
],
},
"templates": [
"slack-template",
"custom-email",
"provisioned-template",
"template with spaces",
],
},
"template_file_provenances": {
"provisioned-template": "api",
},
"template_file_provenances": {},
"template_files": {
"some template": "{{ define 'some template' }} something {{ end }}",
"custom-email": "{{ define 'custom-email' }} Custom email template {{ end }}",
"provisioned-template": "{{ define 'provisioned-template' }} Custom provisioned template {{ end }}",
"slack-template": "{{ define 'slack-template' }} Custom slack template {{ end }}",
"template with spaces": "{{ define 'template with spaces' }} Custom template with spaces in the name {{ end }}",
},
},
]
@@ -0,0 +1,133 @@
import { produce } from 'immer';
import { useDispatch } from 'app/types';
import { AlertManagerCortexConfig } from '../../../../../plugins/datasource/alertmanager/types';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { updateAlertManagerConfigAction } from '../../state/actions';
import { PROVENANCE_NONE } from '../../utils/k8s/constants';
import { ensureDefine } from '../../utils/templates';
import { TemplateFormValues } from '../receivers/TemplateForm';
interface BaseAlertmanagerArgs {
alertmanager: string;
}
export interface NotificationTemplate {
name: string;
template: string;
provenance: string;
}
export function useNotificationTemplates({ alertmanager }: BaseAlertmanagerArgs) {
const { useGetAlertmanagerConfigurationQuery } = alertmanagerApi;
const templatesRequestState = useGetAlertmanagerConfigurationQuery(alertmanager, {
skip: !alertmanager,
selectFromResult: (state) => ({
...state,
data: state.data ? amConfigToTemplates(state.data) : undefined,
currentData: state.currentData ? amConfigToTemplates(state.currentData) : undefined,
}),
});
return templatesRequestState;
}
function amConfigToTemplates(config: AlertManagerCortexConfig): NotificationTemplate[] {
return Object.entries(config.template_files).map(([name, template]) => ({
name,
template,
// Undefined, null or empty string should be converted to PROVENANCE_NONE
provenance: (config.template_file_provenances ?? {})[name] || PROVENANCE_NONE,
}));
}
export function useCreateNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) {
const dispatch = useDispatch();
const { useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi;
const [fetchAmConfig] = useLazyGetAlertmanagerConfigurationQuery();
return async ({ template }: { template: TemplateFormValues }) => {
const amConfig = await fetchAmConfig(alertmanager).unwrap();
// wrap content in "define" if it's not already wrapped, in case user did not do it/
// it's not obvious that this is needed for template to work
const content = ensureDefine(template.name, template.content);
// TODO Check we're NOT overriding an existing template
const updatedConfig = produce(amConfig, (draft) => {
draft.template_files[template.name] = content;
draft.alertmanager_config.templates = [...(draft.alertmanager_config.templates ?? []), template.name];
});
return dispatch(
updateAlertManagerConfigAction({
alertManagerSourceName: alertmanager,
newConfig: updatedConfig,
oldConfig: amConfig,
successMessage: 'Template saved.',
})
).unwrap();
};
}
export function useUpdateNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) {
const dispatch = useDispatch();
const { useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi;
const [fetchAmConfig] = useLazyGetAlertmanagerConfigurationQuery();
return async ({ originalName, template }: { originalName: string; template: TemplateFormValues }) => {
const amConfig = await fetchAmConfig(alertmanager).unwrap();
// wrap content in "define" if it's not already wrapped, in case user did not do it/
// it's not obvious that this is needed for template to work
const content = ensureDefine(template.name, template.content);
const nameChanged = originalName !== template.name;
// TODO Maybe we could simplify or extract this logic
const updatedConfig = produce(amConfig, (draft) => {
if (nameChanged) {
delete draft.template_files[originalName];
draft.alertmanager_config.templates = draft.alertmanager_config.templates?.filter((t) => t !== originalName);
}
draft.template_files[template.name] = content;
draft.alertmanager_config.templates = [...(draft.alertmanager_config.templates ?? []), template.name];
});
return dispatch(
updateAlertManagerConfigAction({
alertManagerSourceName: alertmanager,
newConfig: updatedConfig,
oldConfig: amConfig,
successMessage: 'Template saved.',
})
).unwrap();
};
}
export function useDeleteNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) {
const dispatch = useDispatch();
const { useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi;
const [fetchAmConfig] = useLazyGetAlertmanagerConfigurationQuery();
return async ({ name }: { name: string }) => {
const amConfig = await fetchAmConfig(alertmanager).unwrap();
const updatedConfig = produce(amConfig, (draft) => {
delete draft.template_files[name];
draft.alertmanager_config.templates = draft.alertmanager_config.templates?.filter((t) => t !== name);
});
return dispatch(
updateAlertManagerConfigAction({
alertManagerSourceName: alertmanager,
newConfig: updatedConfig,
oldConfig: amConfig,
successMessage: 'Template deleted.',
})
).unwrap();
};
}
@@ -7,7 +7,7 @@ import { useToggle } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { isFetchError } from '@grafana/runtime';
import { isFetchError, locationService } from '@grafana/runtime';
import {
Alert,
Button,
@@ -21,20 +21,22 @@ import {
InlineField,
Box,
} from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { useCleanup } from 'app/core/hooks/useCleanup';
import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints';
import { AlertManagerCortexConfig, TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
import { AppChromeUpdate } from '../../../../../core/components/AppChrome/AppChromeUpdate';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { updateAlertManagerConfigAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { makeAMLink } from '../../utils/misc';
import { makeAMLink, stringifyErrorLike } from '../../utils/misc';
import { initialAsyncRequestState } from '../../utils/redux';
import { ensureDefine } from '../../utils/templates';
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader';
import {
useCreateNotificationTemplate,
useUpdateNotificationTemplate,
} from '../contact-points/useNotificationTemplates';
import { PayloadEditor } from './PayloadEditor';
import { TemplateDataDocs } from './TemplateDataDocs';
@@ -82,13 +84,17 @@ export const isDuplicating = (location: Location) => location.pathname.endsWith(
*/
export const TemplateForm = ({ existing, alertManagerSourceName, config, provenance }: Props) => {
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
const appNotification = useAppNotification();
const createNewTemplate = useCreateNotificationTemplate({ alertmanager: alertManagerSourceName });
const updateTemplate = useUpdateNotificationTemplate({ alertmanager: alertManagerSourceName });
useCleanup((state) => (state.unifiedAlerting.saveAMConfig = initialAsyncRequestState));
const formRef = useRef<HTMLFormElement>(null);
const isGrafanaAlertManager = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME;
const { loading, error } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
const { error } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
const [cheatsheetOpened, toggleCheatsheetOpened] = useToggle(false);
@@ -111,47 +117,6 @@ export const TemplateForm = ({ existing, alertManagerSourceName, config, provena
dragPosition: 'middle',
});
const submit = (values: TemplateFormValues) => {
// wrap content in "define" if it's not already wrapped, in case user did not do it/
// it's not obvious that this is needed for template to work
const content = ensureDefine(values.name, values.content);
// add new template to template map
const template_files = {
...config.template_files,
[values.name]: content,
};
// delete existing one (if name changed, otherwise it was overwritten in previous step)
if (existing && existing.name !== values.name) {
delete template_files[existing.name];
}
// make sure name for the template is configured on the alertmanager config object
const templates = [
...(config.alertmanager_config.templates ?? []).filter((name) => name !== existing?.name),
values.name,
];
const newConfig: AlertManagerCortexConfig = {
template_files,
alertmanager_config: {
...config.alertmanager_config,
templates,
},
};
dispatch(
updateAlertManagerConfigAction({
alertManagerSourceName,
newConfig,
oldConfig: config,
successMessage: 'Template saved.',
redirectPath: '/alerting/notifications',
redirectSearch: `tab=${ContactPointsActiveTabs.NotificationTemplates}`,
})
);
};
const formApi = useForm<TemplateFormValues>({
mode: 'onSubmit',
defaultValues: existing ?? defaults,
@@ -159,12 +124,29 @@ export const TemplateForm = ({ existing, alertManagerSourceName, config, provena
const {
handleSubmit,
register,
formState: { errors },
formState: { errors, isSubmitting },
getValues,
setValue,
watch,
} = formApi;
const submit = async (values: TemplateFormValues) => {
const returnLink = makeAMLink('/alerting/notifications', alertManagerSourceName, {
tab: ContactPointsActiveTabs.NotificationTemplates,
});
try {
if (!existing) {
await createNewTemplate({ template: values });
} else {
await updateTemplate({ originalName: existing.name, template: values });
}
locationService.push(returnLink);
} catch (error) {
appNotification.error('Error saving template', stringifyErrorLike(error));
}
};
const validateNameIsUnique: Validate<string, TemplateFormValues> = (name: string) => {
return !config.template_files[name] || existing?.name === name
? true
@@ -173,11 +155,11 @@ export const TemplateForm = ({ existing, alertManagerSourceName, config, provena
const actionButtons = (
<Stack>
<Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={loading}>
<Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={isSubmitting}>
Save
</Button>
<LinkButton
disabled={loading}
disabled={isSubmitting}
href={makeAMLink('alerting/notifications', alertManagerSourceName, {
tab: ContactPointsActiveTabs.NotificationTemplates,
})}
@@ -201,7 +183,11 @@ export const TemplateForm = ({ existing, alertManagerSourceName, config, provena
</Alert>
)}
{/* warning about provisioned template */}
{provenance && <ProvisioningAlert resource={ProvisionedResource.Template} />}
{provenance && (
<Box grow={0}>
<ProvisioningAlert resource={ProvisionedResource.Template} />
</Box>
)}
{/* name field for the template */}
<FieldSet disabled={Boolean(provenance)} className={styles.fieldset}>
@@ -1,74 +0,0 @@
import { render, screen, within } from '@testing-library/react';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { locationService } from '@grafana/runtime';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types';
import { grantUserPermissions } from '../../mocks';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { TemplatesTable } from './TemplatesTable';
const defaultConfig: AlertManagerCortexConfig = {
template_files: {
template1: `{{ define "define1" }}`,
},
alertmanager_config: {
templates: ['template1'],
},
};
jest.mock('app/types', () => ({
...jest.requireActual('app/types'),
useDispatch: () => jest.fn(),
}));
jest.mock('app/core/services/context_srv');
const renderWithProvider = () => {
const store = configureStore();
render(
<Provider store={store}>
<Router history={locationService.getHistory()}>
<AlertmanagerProvider accessType={'notification'}>
<TemplatesTable config={defaultConfig} alertManagerName={'potato'} />
</AlertmanagerProvider>
</Router>
</Provider>
);
};
describe('TemplatesTable', () => {
beforeEach(() => {
jest.resetAllMocks();
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsWrite,
AccessControlAction.AlertingNotificationsExternalRead,
AccessControlAction.AlertingNotificationsExternalWrite,
]);
});
it('Should render templates table with the correct rows', () => {
renderWithProvider();
const rows = screen.getAllByRole('row', { name: /template1/i });
expect(within(rows[0]).getByRole('cell', { name: /template1/i })).toBeInTheDocument();
});
it('Should render duplicate template button when having permissions', () => {
renderWithProvider();
const rows = screen.getAllByRole('row', { name: /template1/i });
expect(within(rows[0]).getByRole('cell', { name: /Copy/i })).toBeInTheDocument();
});
it('Should not render duplicate template button when not having write permissions', () => {
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsExternalRead,
]);
renderWithProvider();
const rows = screen.getAllByRole('row', { name: /template1/i });
expect(within(rows[0]).queryByRole('cell', { name: /Copy/i })).not.toBeInTheDocument();
});
});
@@ -1,45 +1,36 @@
import { Fragment, useMemo, useState } from 'react';
import { Fragment, useState } from 'react';
import { ConfirmModal, useStyles2 } from '@grafana/ui';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
import { Authorize } from '../../components/Authorize';
import { AlertmanagerAction } from '../../hooks/useAbilities';
import { deleteTemplateAction } from '../../state/actions';
import { getAlertTableStyles } from '../../styles/table';
import { PROVENANCE_NONE } from '../../utils/k8s/constants';
import { makeAMLink } from '../../utils/misc';
import { CollapseToggle } from '../CollapseToggle';
import { DetailsField } from '../DetailsField';
import { ProvisioningBadge } from '../Provisioning';
import { NotificationTemplate, useDeleteNotificationTemplate } from '../contact-points/useNotificationTemplates';
import { ActionIcon } from '../rules/ActionIcon';
import { TemplateEditor } from './TemplateEditor';
interface Props {
config: AlertManagerCortexConfig;
alertManagerName: string;
templates: NotificationTemplate[];
}
export const TemplatesTable = ({ config, alertManagerName }: Props) => {
const dispatch = useDispatch();
export const TemplatesTable = ({ alertManagerName, templates }: Props) => {
const deleteTemplate = useDeleteNotificationTemplate({ alertmanager: alertManagerName });
const [expandedTemplates, setExpandedTemplates] = useState<Record<string, boolean>>({});
const tableStyles = useStyles2(getAlertTableStyles);
const templateRows = useMemo(() => {
const templates = Object.entries(config.template_files);
return templates.map(([name, template]) => ({
name,
template,
provenance: (config.template_file_provenances ?? {})[name],
}));
}, [config]);
const [templateToDelete, setTemplateToDelete] = useState<string>();
const deleteTemplate = () => {
const onDeleteTemplate = async () => {
if (templateToDelete) {
dispatch(deleteTemplateAction(templateToDelete, alertManagerName));
await deleteTemplate({ name: templateToDelete });
}
setTemplateToDelete(undefined);
};
@@ -68,13 +59,14 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
</tr>
</thead>
<tbody>
{!templateRows.length && (
{!templates.length && (
<tr className={tableStyles.evenRow}>
<td colSpan={3}>No templates defined.</td>
</tr>
)}
{templateRows.map(({ name, template, provenance }, idx) => {
const isExpanded = !!expandedTemplates[name];
{templates.map(({ name, template, provenance }, idx) => {
const isProvisioned = provenance !== PROVENANCE_NONE;
const isExpanded = expandedTemplates[name];
return (
<Fragment key={name}>
<tr key={name} className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
@@ -85,10 +77,10 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
/>
</td>
<td>
{name} {provenance && <ProvisioningBadge />}
{name} {isProvisioned && <ProvisioningBadge />}
</td>
<td className={tableStyles.actionsCell}>
{provenance && (
{isProvisioned && (
<ActionIcon
to={makeAMLink(
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
@@ -98,7 +90,7 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
icon="file-alt"
/>
)}
{!provenance && (
{!isProvisioned && (
<Authorize actions={[AlertmanagerAction.UpdateNotificationTemplate]}>
<ActionIcon
to={makeAMLink(
@@ -120,7 +112,7 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
icon="copy"
/>
</Authorize>
{!provenance && (
{!isProvisioned && (
<Authorize actions={[AlertmanagerAction.DeleteNotificationTemplate]}>
<ActionIcon
onClick={() => setTemplateToDelete(name)}
@@ -163,7 +155,7 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => {
title="Delete template"
body={`Are you sure you want to delete template "${templateToDelete}"?`}
confirmText="Yes, delete"
onConfirm={deleteTemplate}
onConfirm={onDeleteTemplate}
onDismiss={() => setTemplateToDelete(undefined)}
/>
)}
@@ -519,39 +519,6 @@ export const deleteReceiverAction = (receiverName: string, alertManagerSourceNam
};
};
export const deleteTemplateAction = (templateName: string, alertManagerSourceName: string): ThunkResult<void> => {
return async (dispatch) => {
const config = await dispatch(
alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
).unwrap();
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.',
})
);
};
};
export const fetchFolderAction = createAsyncThunk(
'unifiedalerting/fetchFolder',
(uid: string): Promise<FolderDTO> => withSerializedError(backendSrv.getFolderByUid(uid, { withAccessControl: true }))