Alerting: Fix template editing issues for contact points (#95387)

pull/95429/head
Tom Ratcliffe 9 months ago committed by GitHub
parent b2de69d741
commit b0e116c5fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      public/app/features/alerting/unified/api/templateApi.ts
  2. 76
      public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx
  3. 4
      public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap
  4. 10
      public/app/features/alerting/unified/components/contact-points/templates/EditorColumnHeader.tsx
  5. 2
      public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap
  6. 12
      public/app/features/alerting/unified/components/receivers/form/fields/TemplateContentAndPreview.tsx
  7. 125
      public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx
  8. 2
      public/app/features/alerting/unified/mockGrafanaNotifiers.ts
  9. 2
      public/app/features/alerting/unified/mocks/server/entities/alertmanager-config/grafana-alertmanager-config.ts
  10. 15
      public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts
  11. 1
      public/locales/en-US/grafana.json
  12. 1
      public/locales/pseudo-LOCALE/grafana.json

@ -41,9 +41,11 @@ generatedTemplatesApi.enhanceEndpoints({
}, },
}); });
export type TemplatesTestPayload = { template: string; alerts: AlertField[]; name: string };
export const templatesApi = generatedTemplatesApi.injectEndpoints({ export const templatesApi = generatedTemplatesApi.injectEndpoints({
endpoints: (build) => ({ endpoints: (build) => ({
previewTemplate: build.mutation<TemplatePreviewResponse, { template: string; alerts: AlertField[]; name: string }>({ previewTemplate: build.mutation<TemplatePreviewResponse, TemplatesTestPayload>({
query: ({ template, alerts, name }) => ({ query: ({ template, alerts, name }) => ({
url: previewTemplateUrl, url: previewTemplateUrl,
data: { template: template, alerts: alerts, name: name }, data: { template: template, alerts: alerts, name: name },

@ -0,0 +1,76 @@
import 'core-js/stable/structured-clone';
import { Routes, Route } from 'react-router-dom-v5-compat';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
import { render, screen } from 'test/test-utils';
import EditContactPoint from 'app/features/alerting/unified/components/contact-points/EditContactPoint';
import { AccessControlAction } from 'app/types';
import { setupMswServer } from '../../mockApi';
import { grantUserPermissions } from '../../mocks';
setupMswServer();
const Index = () => {
return <div>redirected</div>;
};
const renderEditContactPoint = (contactPointUid: string) =>
render(
<Routes>
<Route path="/alerting/notifications" element={<Index />} />
<Route path="/alerting/notifications/receivers/:name/edit" element={<EditContactPoint />} />
</Routes>,
{
historyOptions: { initialEntries: [`/alerting/notifications/receivers/${contactPointUid}/edit`] },
}
);
beforeEach(() => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite]);
});
const getTemplatePreviewContent = async () =>
await screen.findByRole('presentation', { description: /Preview with the default payload/i });
const templatesSelectorTestId = 'existing-templates-selector';
describe('Edit contact point', () => {
it('can edit a contact point with existing template field values', async () => {
const { user } = renderEditContactPoint('lotsa-emails');
// Expand settings and open "edit message template" drawer
await user.click(await screen.findByText(/optional email settings/i));
await user.click(await screen.findByRole('button', { name: /edit message/i }));
expect(await screen.findByRole('dialog', { name: /edit message/i })).toBeInTheDocument();
expect(await getTemplatePreviewContent()).toHaveTextContent(/some example preview for slack-template/i);
// Change the preset template and check that the preview updates correctly
await clickSelectOption(screen.getByTestId(templatesSelectorTestId), 'custom-email');
expect(await getTemplatePreviewContent()).toHaveTextContent(/some example preview for custom-email/i);
// Close the drawer
await user.click(screen.getByRole('button', { name: /^save$/i }));
// Check a setting that has an existing custom value, and change it to a preset template
await user.click(await screen.findByRole('button', { name: /edit subject/i }));
expect(await screen.findByRole('dialog', { name: /edit subject/i })).toBeInTheDocument();
// If this isn't correct, then we haven't set the correct initial state for the radio buttons/tabs
expect(await screen.findByLabelText(/custom template value/i)).toHaveValue('some custom value');
await user.click(screen.getByRole('radio', { name: /select existing template/i }));
await clickSelectOption(screen.getByTestId(templatesSelectorTestId), 'slack-template');
expect(await getTemplatePreviewContent()).toHaveTextContent(/some example preview for slack-template/i);
// Close the drawer
await user.click(screen.getByRole('button', { name: /^save$/i }));
expect(await screen.findByText(/template: custom-email/i)).toBeInTheDocument();
expect(await screen.findByText(/template: slack-template/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /save contact point/i }));
expect(await screen.findByText(/redirected/i)).toBeInTheDocument();
});
});

@ -49,7 +49,9 @@ exports[`useContactPoints should return contact points with status 1`] = `
"secureFields": {}, "secureFields": {},
"settings": { "settings": {
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
"message": "{{ template "slack-template" . }}",
"singleEmail": false, "singleEmail": false,
"subject": "some custom value",
}, },
"type": "email", "type": "email",
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",
@ -243,7 +245,9 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
"secureFields": {}, "secureFields": {},
"settings": { "settings": {
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
"message": "{{ template "slack-template" . }}",
"singleEmail": false, "singleEmail": false,
"subject": "some custom value",
}, },
"type": "email", "type": "email",
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",

@ -4,12 +4,16 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Label, Stack, useStyles2 } from '@grafana/ui'; import { Label, Stack, useStyles2 } from '@grafana/ui';
export function EditorColumnHeader({ label, actions }: { label: string; actions?: React.ReactNode }) { type Props = { label: string; actions?: React.ReactNode; id?: string };
export function EditorColumnHeader({ label, actions, id }: Props) {
const styles = useStyles2(editorColumnStyles); const styles = useStyles2(editorColumnStyles);
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Label className={styles.label}>{label}</Label> <Label className={styles.label} id={id}>
{label}
</Label>
<Stack direction="row" gap={1}> <Stack direction="row" gap={1}>
{actions} {actions}
</Stack> </Stack>
@ -17,7 +21,7 @@ export function EditorColumnHeader({ label, actions }: { label: string; actions?
); );
} }
export const editorColumnStyles = (theme: GrafanaTheme2) => ({ const editorColumnStyles = (theme: GrafanaTheme2) => ({
container: css({ container: css({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',

@ -76,7 +76,9 @@ exports[`alerting API server disabled should be able to test and save a receiver
"secureFields": {}, "secureFields": {},
"settings": { "settings": {
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
"message": "{{ template "slack-template" . }}",
"singleEmail": false, "singleEmail": false,
"subject": "some custom value",
}, },
"type": "email", "type": "email",
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",

@ -35,6 +35,8 @@ export function TemplateContentAndPreview({
const { data, error } = usePreviewTemplate(templateContent, templateName, payload, setPayloadFormatError); const { data, error } = usePreviewTemplate(templateContent, templateName, payload, setPayloadFormatError);
const previewToRender = getPreviewResults(error, payloadFormatError, data); const previewToRender = getPreviewResults(error, payloadFormatError, data);
const templatePreviewId = 'template-preview';
return ( return (
<div className={cx(className, styles.mainContainer)}> <div className={cx(className, styles.mainContainer)}>
<div className={styles.container}> <div className={styles.container}>
@ -58,9 +60,15 @@ export function TemplateContentAndPreview({
{isGrafanaAlertManager && ( {isGrafanaAlertManager && (
<div className={styles.container}> <div className={styles.container}>
<EditorColumnHeader label="Preview with the default payload" /> <EditorColumnHeader id={templatePreviewId} label="Preview with the default payload" />
<Box flex={1}> <Box flex={1}>
<div className={styles.viewerContainer({ height: 'minHeight' })}>{previewToRender}</div> <div
role="presentation"
aria-describedby={templatePreviewId}
className={styles.viewerContainer({ height: 'minHeight' })}
>
{previewToRender}
</div>
</Box> </Box>
</div> </div>
)} )}

@ -8,7 +8,7 @@ import {
Button, Button,
Drawer, Drawer,
IconButton, IconButton,
Input, Label,
RadioButtonGroup, RadioButtonGroup,
Select, Select,
Stack, Stack,
@ -16,6 +16,7 @@ import {
TextArea, TextArea,
useStyles2, useStyles2,
} from '@grafana/ui'; } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { import {
trackEditInputWithTemplate, trackEditInputWithTemplate,
trackUseCustomInputInTemplate, trackUseCustomInputInTemplate,
@ -34,6 +35,8 @@ import { defaultPayloadString } from '../../TemplateForm';
import { TemplateContentAndPreview } from './TemplateContentAndPreview'; import { TemplateContentAndPreview } from './TemplateContentAndPreview';
import { getTemplateName, getUseTemplateText, matchesOnlyOneTemplate, parseTemplates } from './utils'; import { getTemplateName, getUseTemplateText, matchesOnlyOneTemplate, parseTemplates } from './utils';
const { useGetDefaultTemplatesQuery } = templatesApi;
interface TemplatesPickerProps { interface TemplatesPickerProps {
onSelect: (temnplate: string) => void; onSelect: (temnplate: string) => void;
option: NotificationChannelOption; option: NotificationChannelOption;
@ -45,27 +48,23 @@ export function TemplatesPicker({ onSelect, option, valueInForm }: TemplatesPick
setShowTemplates(true); setShowTemplates(true);
trackEditInputWithTemplate(); trackEditInputWithTemplate();
}; };
const handleClose = () => setShowTemplates(false);
return ( return (
<> <>
<Button <Button
icon="edit" icon="edit"
tooltip={'Edit using existing templates.'} tooltip={`Edit ${option.label.toLowerCase()} using existing templates.`}
onClick={onClick} onClick={onClick}
variant="secondary" variant="secondary"
size="sm" size="sm"
aria-label={'Select available template from the list of available templates.'}
> >
{`Edit ${option.label}`} {`Edit ${option.label}`}
</Button> </Button>
{showTemplates && ( {showTemplates && (
<Drawer title={`Edit ${option.label}`} size="md" onClose={() => setShowTemplates(false)}> <Drawer title={`Edit ${option.label}`} size="md" onClose={handleClose}>
<TemplateSelector <TemplateSelector onSelect={onSelect} onClose={handleClose} option={option} valueInForm={valueInForm} />
onSelect={onSelect}
onClose={() => setShowTemplates(false)}
option={option}
valueInForm={valueInForm}
/>
</Drawer> </Drawer>
)} )}
</> </>
@ -102,10 +101,6 @@ export function getTemplateOptions(templateFiles: NotificationTemplate[], defaul
// return the sum of default and custom templates // return the sum of default and custom templates
return Array.from(templateMap.values()); return Array.from(templateMap.values());
} }
function getContentFromOptions(name: string, options: Array<SelectableValue<Template>>) {
const template = options.find((option) => option.label === name);
return template?.value?.content ?? '';
}
export interface Template { export interface Template {
name: string; name: string;
@ -117,40 +112,44 @@ interface TemplateSelectorProps {
option: NotificationChannelOption; option: NotificationChannelOption;
valueInForm: string; valueInForm: string;
} }
function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSelectorProps) { function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSelectorProps) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const useGetDefaultTemplatesQuery = templatesApi.endpoints.getDefaultTemplates.useQuery; const valueInFormIsCustom = Boolean(valueInForm) && !matchesOnlyOneTemplate(valueInForm);
const [template, setTemplate] = useState<Template | undefined>(undefined); const [template, setTemplate] = useState<SelectableValue<Template> | undefined>(undefined);
const [inputToUpdate, setInputToUpdate] = useState<string>(''); const [customTemplateValue, setCustomTemplateValue] = useState<string>(valueInForm);
const [inputToUpdateCustom, setInputToUpdateCustom] = useState<string>(valueInForm);
const { selectedAlertmanager } = useAlertmanager(); const { selectedAlertmanager } = useAlertmanager();
const { data = [], error, isLoading } = useNotificationTemplates({ alertmanager: selectedAlertmanager! }); const { data = [], error, isLoading } = useNotificationTemplates({ alertmanager: selectedAlertmanager! });
const { data: defaultTemplates } = useGetDefaultTemplatesQuery(); const { data: defaultTemplates } = useGetDefaultTemplatesQuery();
const [templateOption, setTemplateOption] = useState<TemplateFieldOption>('Existing'); const [templateOption, setTemplateOption] = useState<TemplateFieldOption | undefined>(
valueInFormIsCustom ? 'Custom' : 'Existing'
);
const [_, copyToClipboard] = useCopyToClipboard(); const [_, copyToClipboard] = useCopyToClipboard();
const templateOptions: Array<SelectableValue<TemplateFieldOption>> = [ const templateOptions: Array<SelectableValue<TemplateFieldOption>> = [
{ {
label: 'Select existing template', label: 'Select existing template',
ariaLabel: 'Select existing template',
value: 'Existing', value: 'Existing',
description: `Select a single template and preview it, or copy it to paste it in the custom tab. ${templateOption === 'Existing' ? 'Clicking Save will save your changes to the selected template.' : ''}`, description: `Select a single template and preview it, or copy it to paste it in the custom tab. ${templateOption === 'Existing' ? 'Clicking Save will save your changes to the selected template.' : ''}`,
}, },
{ {
label: `Enter custom ${option.label.toLowerCase()}`, label: `Enter custom ${option.label.toLowerCase()}`,
ariaLabel: `Enter custom ${option.label.toLowerCase()}`,
value: 'Custom', value: 'Custom',
description: `Enter custom ${option.label.toLowerCase()}. ${templateOption === 'Custom' ? 'Clicking Save will save the custom value only.' : ''}`, description: `Enter custom ${option.label.toLowerCase()}. ${templateOption === 'Custom' ? 'Clicking Save will save the custom value only.' : ''}`,
}, },
]; ];
useEffect(() => { useEffect(() => {
if (template) { if (template?.value?.name) {
setInputToUpdate(getUseTemplateText(template.name)); setCustomTemplateValue(getUseTemplateText(template.value.name));
} }
}, [template]); }, [template]);
function onCustomTemplateChange(customInput: string) { function onCustomTemplateChange(customInput: string) {
setInputToUpdateCustom(customInput); setCustomTemplateValue(customInput);
} }
const onTemplateOptionChange = (option: TemplateFieldOption) => { const onTemplateOptionChange = (option: TemplateFieldOption) => {
@ -164,21 +163,14 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
return getTemplateOptions(data, defaultTemplates); return getTemplateOptions(data, defaultTemplates);
}, [data, defaultTemplates, isLoading, error]); }, [data, defaultTemplates, isLoading, error]);
// if we are using only one template, we should settemplate to that template const defaultTemplateValue = useMemo(() => {
useEffect(() => { if (!options.length || !Boolean(valueInForm) || !matchesOnlyOneTemplate(valueInForm)) {
if (Boolean(valueInForm)) { return null;
if (matchesOnlyOneTemplate(valueInForm)) {
const name = getTemplateName(valueInForm);
setTemplate({
name,
content: getContentFromOptions(name, options),
});
} else {
// if it's empty we default to select existing template
setTemplateOption('Custom');
}
} }
}, [valueInForm, setTemplate, setTemplateOption, options]); const nameOfTemplateInForm = getTemplateName(valueInForm);
return options.find((option) => option.label === nameOfTemplateInForm) || null;
}, [options, valueInForm]);
if (error) { if (error) {
return <div>Error loading templates</div>; return <div>Error loading templates</div>;
@ -202,26 +194,29 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
<Stack direction="column" gap={1}> <Stack direction="column" gap={1}>
<Stack direction="row" gap={1} alignItems="center"> <Stack direction="row" gap={1} alignItems="center">
<Select<Template> <Select<Template>
data-testid="existing-templates-selector"
placeholder="Choose template" placeholder="Choose template"
aria-label="Choose template" aria-label="Choose template"
onChange={(value: SelectableValue<Template>, _) => { onChange={(value: SelectableValue<Template>, _) => {
setTemplate(value?.value); setTemplate(value);
}} }}
options={options} options={options}
width={50} width={50}
value={template ? { label: template.name, value: template } : undefined} defaultValue={defaultTemplateValue}
/> />
<IconButton <IconButton
tooltip="Copy selected template to clipboard. You can use it in the custom tab." tooltip="Copy selected template to clipboard. You can use it in the custom tab."
onClick={() => copyToClipboard(getUseTemplateText(template?.name ?? ''))} onClick={() =>
copyToClipboard(getUseTemplateText(template?.value?.name ?? defaultTemplateValue?.value?.name ?? ''))
}
name="copy" name="copy"
/> />
</Stack> </Stack>
<TemplateContentAndPreview <TemplateContentAndPreview
templateContent={template?.content ?? ''} templateContent={template?.value?.content ?? defaultTemplateValue?.value?.content ?? ''}
payload={defaultPayloadString} payload={defaultPayloadString}
templateName={template?.name ?? ''} templateName={template?.value?.name ?? defaultTemplateValue?.value?.name ?? ''}
setPayloadFormatError={() => {}} setPayloadFormatError={() => {}}
className={cx(styles.templatePreview, styles.minEditorSize)} className={cx(styles.templatePreview, styles.minEditorSize)}
payloadFormatError={null} payloadFormatError={null}
@ -231,7 +226,7 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
<OptionCustomfield <OptionCustomfield
option={option} option={option}
onCustomTemplateChange={onCustomTemplateChange} onCustomTemplateChange={onCustomTemplateChange}
initialValue={inputToUpdateCustom} initialValue={customTemplateValue}
/> />
)} )}
</Stack> </Stack>
@ -242,13 +237,15 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
<Button <Button
variant="primary" variant="primary"
onClick={() => { onClick={() => {
onSelect(templateOption === 'Custom' ? inputToUpdateCustom : inputToUpdate);
onClose();
if (templateOption === 'Custom') { if (templateOption === 'Custom') {
trackUseCustomInputInTemplate(); trackUseCustomInputInTemplate();
onSelect(customTemplateValue);
} else { } else {
trackUseSingleTemplateInInput(); trackUseSingleTemplateInInput();
const name = template?.value?.name ?? defaultTemplateValue?.value?.name ?? '';
onSelect(getUseTemplateText(name));
} }
return onClose();
}} }}
> >
Save Save
@ -267,31 +264,21 @@ function OptionCustomfield({
onCustomTemplateChange(customInput: string): void; onCustomTemplateChange(customInput: string): void;
initialValue: string; initialValue: string;
}) { }) {
switch (option.element) { const id = `custom-template-${option.label}`;
case 'textarea': return (
return ( <Stack direction="column" gap={1}>
<Stack direction="row" gap={1} alignItems="center"> <Label htmlFor={id}>
<TextArea <Trans i18nKey="alerting.contact-points.custom-template-value">Custom template value</Trans>
placeholder={option.placeholder} </Label>
onChange={(e) => onCustomTemplateChange(e.currentTarget.value)} <TextArea
defaultValue={initialValue} id={id}
/> label="Custom template"
</Stack> placeholder={option.placeholder}
); onChange={(e) => onCustomTemplateChange(e.currentTarget.value)}
case 'input': defaultValue={initialValue}
return ( />
<Stack direction="row" gap={1} alignItems="center"> </Stack>
<Input );
type={option.inputType}
placeholder={option.placeholder}
onChange={(e) => onCustomTemplateChange(e.currentTarget.value)}
defaultValue={initialValue}
/>
</Stack>
);
default:
return null;
}
} }
interface WrapWithTemplateSelectionProps extends PropsWithChildren { interface WrapWithTemplateSelectionProps extends PropsWithChildren {

@ -289,7 +289,7 @@ export const grafanaAlertNotifiersMock: NotifierDTO[] = [
label: 'Message', label: 'Message',
description: description:
'Optional message. You can use templates to customize this field. Using a custom message will replace the default message', 'Optional message. You can use templates to customize this field. Using a custom message will replace the default message',
placeholder: '', placeholder: '{{ template "default.message" . }}',
propertyName: 'message', propertyName: 'message',
selectOptions: null, selectOptions: null,
showWhen: { showWhen: {

@ -124,6 +124,8 @@ const grafanaAlertmanagerConfig: AlertManagerCortexConfig = {
addresses: addresses:
'gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com', 'gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com',
singleEmail: false, singleEmail: false,
message: '{{ template "slack-template" . }}',
subject: 'some custom value',
}, },
secureFields: {}, secureFields: {},
}, },

@ -1,5 +1,6 @@
import { http, HttpResponse, JsonBodyType, StrictResponse } from 'msw'; import { http, HttpResponse, JsonBodyType, StrictResponse } from 'msw';
import { TemplatesTestPayload } from 'app/features/alerting/unified/api/templateApi';
import receiversMock from 'app/features/alerting/unified/components/contact-points/__mocks__/receivers.mock.json'; import receiversMock from 'app/features/alerting/unified/components/contact-points/__mocks__/receivers.mock.json';
import { MOCK_SILENCE_ID_EXISTING, mockAlertmanagerAlert } from 'app/features/alerting/unified/mocks'; import { MOCK_SILENCE_ID_EXISTING, mockAlertmanagerAlert } from 'app/features/alerting/unified/mocks';
import { defaultGrafanaAlertingConfigurationStatusResponse } from 'app/features/alerting/unified/mocks/alertmanagerApi'; import { defaultGrafanaAlertingConfigurationStatusResponse } from 'app/features/alerting/unified/mocks/alertmanagerApi';
@ -135,9 +136,17 @@ export const updateAlertmanagerConfigHandler = (responseOverride?: typeof ALERTM
}); });
const getGrafanaAlertmanagerTemplatePreview = () => const getGrafanaAlertmanagerTemplatePreview = () =>
http.post('/api/alertmanager/grafana/config/api/v1/templates/test', () => http.post<never, TemplatesTestPayload>(
// TODO: Scaffold out template preview response as needed by tests '/api/alertmanager/grafana/config/api/v1/templates/test',
HttpResponse.json({}) async ({ request }) => {
const body = await request.json();
if (body?.template.startsWith('{{')) {
return HttpResponse.json({ results: [{ name: 'asdasd', text: `some example preview for ${body.name}` }] });
}
return HttpResponse.json({});
}
); );
const getReceiversHandler = () => const getReceiversHandler = () =>

@ -140,6 +140,7 @@
"contact-point": "Contact Point", "contact-point": "Contact Point",
"contact-points": { "contact-points": {
"create": "Create contact point", "create": "Create contact point",
"custom-template-value": "Custom template value",
"delete-reasons": { "delete-reasons": {
"heading": "Contact point cannot be deleted for the following reasons:", "heading": "Contact point cannot be deleted for the following reasons:",
"no-permissions": "You do not have the required permission to delete this contact point", "no-permissions": "You do not have the required permission to delete this contact point",

@ -140,6 +140,7 @@
"contact-point": "Cőʼnŧäčŧ Pőįʼnŧ", "contact-point": "Cőʼnŧäčŧ Pőįʼnŧ",
"contact-points": { "contact-points": {
"create": "Cřęäŧę čőʼnŧäčŧ pőįʼnŧ", "create": "Cřęäŧę čőʼnŧäčŧ pőįʼnŧ",
"custom-template-value": "Cūşŧőm ŧęmpľäŧę väľūę",
"delete-reasons": { "delete-reasons": {
"heading": "Cőʼnŧäčŧ pőįʼnŧ čäʼnʼnőŧ þę đęľęŧęđ ƒőř ŧĥę ƒőľľőŵįʼnģ řęäşőʼnş:", "heading": "Cőʼnŧäčŧ pőįʼnŧ čäʼnʼnőŧ þę đęľęŧęđ ƒőř ŧĥę ƒőľľőŵįʼnģ řęäşőʼnş:",
"no-permissions": "Ÿőū đő ʼnőŧ ĥävę ŧĥę řęqūįřęđ pęřmįşşįőʼn ŧő đęľęŧę ŧĥįş čőʼnŧäčŧ pőįʼnŧ", "no-permissions": "Ÿőū đő ʼnőŧ ĥävę ŧĥę řęqūįřęđ pęřmįşşįőʼn ŧő đęľęŧę ŧĥįş čőʼnŧäčŧ pőįʼnŧ",

Loading…
Cancel
Save