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:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+73
@@ -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();
|
||||
});
|
||||
});
|
||||
+12
-6
@@ -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;
|
||||
|
||||
+9
-2
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -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 }}",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
+133
@@ -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 }))
|
||||
|
||||
Reference in New Issue
Block a user