From 9c5a91009bb0cf7f90e1ba871912f0ffbfd30d66 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 7 Aug 2024 11:50:07 +0200 Subject: [PATCH] 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 --- .../alerting/unified/Templates.test.tsx | 31 +--- .../NotificationTemplates.test.tsx | 73 ++++++++++ .../contact-points/NotificationTemplates.tsx | 18 ++- .../__mocks__/alertmanager.config.mock.json | 11 +- .../NewContactPoint.test.tsx.snap | 15 +- .../useNotificationTemplates.ts | 133 ++++++++++++++++++ .../components/receivers/TemplateForm.tsx | 90 +++++------- .../receivers/TemplatesTable.test.tsx | 74 ---------- .../components/receivers/TemplatesTable.tsx | 44 +++--- .../alerting/unified/state/actions.ts | 33 ----- 10 files changed, 298 insertions(+), 224 deletions(-) create mode 100644 public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx create mode 100644 public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts delete mode 100644 public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx diff --git a/public/app/features/alerting/unified/Templates.test.tsx b/public/app/features/alerting/unified/Templates.test.tsx index 2ffdcec94c1..a18a74475b7 100644 --- a/public/app/features/alerting/unified/Templates.test.tsx +++ b/public/app/features/alerting/unified/Templates.test.tsx @@ -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(, { 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(, { - 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(); - }); }); diff --git a/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx b/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx new file mode 100644 index 00000000000..96d298ddcbe --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx @@ -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( + + + + ); +}; + +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(); + }); +}); diff --git a/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.tsx b/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.tsx index 51fd67657b7..7231d06aaed 100644 --- a/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.tsx +++ b/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.tsx @@ -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 {String(error)}; + return {stringifyErrorLike(error)}; } - if (data) { - return ; + if (isLoading) { + return ; + } + + if (templates) { + return ; } return null; diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json index eef90f59764..5038af78a6c 100644 --- a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json @@ -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"] } } diff --git a/public/app/features/alerting/unified/components/contact-points/__snapshots__/NewContactPoint.test.tsx.snap b/public/app/features/alerting/unified/components/contact-points/__snapshots__/NewContactPoint.test.tsx.snap index 8a4a47ab936..4da1caff9e4 100644 --- a/public/app/features/alerting/unified/components/contact-points/__snapshots__/NewContactPoint.test.tsx.snap +++ b/public/app/features/alerting/unified/components/contact-points/__snapshots__/NewContactPoint.test.tsx.snap @@ -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 }}", }, }, ] diff --git a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts new file mode 100644 index 00000000000..35577db9d5f --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts @@ -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(); + }; +} diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index 31e25736564..5dab9e79a87 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -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(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({ 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 = (name: string) => { return !config.template_files[name] || existing?.name === name ? true @@ -173,11 +155,11 @@ export const TemplateForm = ({ existing, alertManagerSourceName, config, provena const actionButtons = ( - )} {/* warning about provisioned template */} - {provenance && } + {provenance && ( + + + + )} {/* name field for the template */}
diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx deleted file mode 100644 index e47d08dfda7..00000000000 --- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.test.tsx +++ /dev/null @@ -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( - - - - - - - - ); -}; - -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(); - }); -}); diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx index 8982cdfcac1..8e9e039401d 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx @@ -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>({}); 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(); - 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) => { - {!templateRows.length && ( + {!templates.length && ( No templates defined. )} - {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 ( @@ -85,10 +77,10 @@ export const TemplatesTable = ({ config, alertManagerName }: Props) => { /> - {name} {provenance && } + {name} {isProvisioned && } - {provenance && ( + {isProvisioned && ( { icon="file-alt" /> )} - {!provenance && ( + {!isProvisioned && ( { icon="copy" /> - {!provenance && ( + {!isProvisioned && ( 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)} /> )} diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index c9bc1ca5ebc..a1c3ad5afa6 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -519,39 +519,6 @@ export const deleteReceiverAction = (receiverName: string, alertManagerSourceNam }; }; -export const deleteTemplateAction = (templateName: string, alertManagerSourceName: string): ThunkResult => { - 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 => withSerializedError(backendSrv.getFolderByUid(uid, { withAccessControl: true }))