import { css, cx } from '@emotion/css'; import { addMinutes, subDays, subHours } from 'date-fns'; import { Location } from 'history'; import { useRef, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useToggle } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, useTranslate } from '@grafana/i18n'; import { isFetchError, locationService } from '@grafana/runtime'; import { Alert, Box, Button, Drawer, Dropdown, FieldSet, InlineField, Input, LinkButton, Menu, Stack, Text, useSplitter, useStyles2, } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints'; import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { makeAMLink, stringifyErrorLike } from '../../utils/misc'; import { ProvisionedResource, ProvisioningAlert } from '../Provisioning'; import { Spacer } from '../Spacer'; import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader'; import { NotificationTemplate, useCreateNotificationTemplate, useNotificationTemplateMetadata, useUpdateNotificationTemplate, useValidateNotificationTemplate, } from '../contact-points/useNotificationTemplates'; import { PayloadEditor } from './PayloadEditor'; import { TemplateDataDocs } from './TemplateDataDocs'; import { GlobalTemplateDataExamples } from './TemplateDataExamples'; import { TemplateEditor } from './TemplateEditor'; import { TemplatePreview } from './TemplatePreview'; import { snippets } from './editor/templateDataSuggestions'; export interface TemplateFormValues { title: string; content: string; } export const defaults: TemplateFormValues = Object.freeze({ title: '', content: '', }); interface Props { originalTemplate?: NotificationTemplate; prefill?: TemplateFormValues; alertmanager: string; } export const isDuplicating = (location: Location) => location.pathname.endsWith('/duplicate'); /** * We're going for this type of layout, but with the ability to resize the columns. * To achieve this, we're using the useSplitter hook from Grafana UI twice. * The first hook is for the vertical splitter between the template editor and the payload editor. * The second hook is for the horizontal splitter between the template editor and the preview. * If we're using a vanilla Alertmanager source, we don't show the payload editor nor the preview but we still use the splitter at 100/0. * * ┌───────────────────┐┌───────────┐ * │ Template ││ Preview │ * │ ││ │ * │ ││ │ * │ ││ │ * └───────────────────┘│ │ * ┌───────────────────┐│ │ * │ Payload ││ │ * │ ││ │ * │ ││ │ * │ ││ │ * └───────────────────┘└───────────┘ */ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) => { const styles = useStyles2(getStyles); const appNotification = useAppNotification(); const [createNewTemplate, { error: createTemplateError }] = useCreateNotificationTemplate({ alertmanager }); const [updateTemplate, { error: updateTemplateError }] = useUpdateNotificationTemplate({ alertmanager }); const { titleIsUnique } = useValidateNotificationTemplate({ alertmanager, originalTemplate }); const formRef = useRef(null); const isGrafanaAlertManager = alertmanager === GRAFANA_RULES_SOURCE_NAME; const error = updateTemplateError ?? createTemplateError; const [cheatsheetOpened, toggleCheatsheetOpened] = useToggle(false); const [payload, setPayload] = useState(defaultPayloadString); const [payloadFormatError, setPayloadFormatError] = useState(null); const { isProvisioned } = useNotificationTemplateMetadata(originalTemplate); const originalTemplatePrefill: TemplateFormValues | undefined = originalTemplate ? { title: originalTemplate.title, content: originalTemplate.content } : undefined; // splitter for template and payload editor const columnSplitter = useSplitter({ direction: 'column', // if Grafana Alertmanager, split 50/50, otherwise 100/0 because there is no payload editor initialSize: isGrafanaAlertManager ? 0.5 : 1, dragPosition: 'middle', }); // splitter for template editor and preview const rowSplitter = useSplitter({ direction: 'row', // if Grafana Alertmanager, split 60/40, otherwise 100/0 because there is no preview initialSize: isGrafanaAlertManager ? 0.6 : 1, dragPosition: 'middle', }); const formApi = useForm({ mode: 'onSubmit', defaultValues: prefill ?? originalTemplatePrefill ?? defaults, }); const { handleSubmit, register, formState: { errors, isSubmitting }, getValues, setValue, watch, } = formApi; const { t } = useTranslate(); const submit = async (values: TemplateFormValues) => { const returnLink = makeAMLink('/alerting/notifications', alertmanager, { tab: ContactPointsActiveTabs.NotificationTemplates, }); try { if (!originalTemplate) { await createNewTemplate.execute({ templateValues: values }); } else { await updateTemplate.execute({ template: originalTemplate, patch: values }); } appNotification.success('Template saved', `Template ${values.title} has been saved`); locationService.push(returnLink); } catch (error) { appNotification.error('Error saving template', stringifyErrorLike(error)); } }; const appendExample = (example: string) => { const content = getValues('content'), newValue = !content ? example : `${content}\n${example}`; setValue('content', newValue); }; return ( <>
{/* error message */} {error && ( {error.message || (isFetchError(error) && error.data?.message) || String(error)} )} {/* warning about provisioned template */} {isProvisioned && ( )} {/* name field for the template */}
{/* name and save buttons */} Cancel {/* editor layout */}
{/* template content and payload editor column – full height and half-width */}
{/* template editor */}
{/* primaryProps will set "minHeight: min-content;" so we have to make sure to apply minHeight to the child */}
{/* examples dropdown – only available for Grafana Alertmanager */} {isGrafanaAlertManager && ( {GlobalTemplateDataExamples.map((item, index) => ( appendExample(item.example)} /> ))} } > )} } />
{({ width, height }) => ( setValue('content', value)} containerStyles={styles.editorContainer} width={width} height={height} /> )}
{/* payload editor – only available for Grafana Alertmanager */} {isGrafanaAlertManager && ( <>
)}
{/* preview column – full height and half-width */} {isGrafanaAlertManager && (
)}
{cheatsheetOpened && ( )} ); }; function TemplatingBasics() { const styles = useStyles2(getStyles); const { t } = useTranslate(); const intro = t( 'alerting.templates.help.intro', `Notification templates use Go templating language to create notification messages. In Grafana, a template group can define multiple notification templates using {{ define "" }}. These templates can then be used in contact points and within other notification templates by calling {{ template "" }}. For detailed information about notification templates, refer to our documentation.` ); return (
{intro}
Notification templates documentation
For auto-completion of common templating code, type the following keywords in the content editor:
{Object.values(snippets) .map((s) => s.label) .join(', ')}
); } function TemplatingCheatSheet() { return ( ); } export const getStyles = (theme: GrafanaTheme2) => { const narrowScreenQuery = theme.breakpoints.down('md'); return { flexFull: css({ flex: 1, }), minEditorSize: css({ minHeight: 300, minWidth: 300, }), payloadEditor: css({ minHeight: 0, }), containerWithBorderAndRadius: css({ borderRadius: theme.shape.radius.default, border: `1px solid ${theme.colors.border.medium}`, }), flexColumn: css({ display: 'flex', flex: 1, flexDirection: 'column', }), form: css({ label: 'template-form', height: '100%', display: 'flex', flexDirection: 'column', }), fieldset: css({ label: 'template-fieldset', flex: 1, display: 'flex', flexDirection: 'column', }), label: css({ margin: 0, }), contentContainer: css({ flex: 1, display: 'flex', flexDirection: 'row', }), contentField: css({ display: 'flex', flexDirection: 'column', flex: 1, marginBottom: 0, }), templatePreview: css({ flex: 1, display: 'flex', }), templatePayload: css({ flex: 1, }), editorContainer: css({ width: 'fit-content', border: 'none', }), payloadCollapseButton: css({ backgroundColor: theme.colors.info.transparent, margin: 0, [narrowScreenQuery]: { display: 'none', }, }), code: css({ color: theme.colors.text.secondary, fontWeight: theme.typography.fontWeightBold, }), }; }; const defaultPayload: TestTemplateAlert[] = [ { status: 'firing', annotations: { summary: 'Instance instance1 has been down for more than 5 minutes', }, labels: { alertname: 'InstanceDown', instance: 'instance1', }, startsAt: subDays(new Date(), 1).toISOString(), endsAt: addMinutes(new Date(), 5).toISOString(), fingerprint: 'a5331f0d5a9d81d4', generatorURL: 'http://grafana.com/alerting/grafana/cdeqmlhvflz40f/view', }, { status: 'resolved', annotations: { summary: 'CPU usage above 90%', }, labels: { alertname: 'CpuUsage', instance: 'instance1', }, startsAt: subHours(new Date(), 4).toISOString(), endsAt: new Date().toISOString(), fingerprint: 'b77d941310f9d381', generatorURL: 'http://grafana.com/alerting/grafana/oZSMdGj7z/view', }, ]; export const defaultPayloadString = JSON.stringify(defaultPayload, null, 2);