From 9f9c4b3da32bb0ab260301cea077a30eeeaf5f53 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Fri, 11 Apr 2025 09:27:19 -0400 Subject: [PATCH] Alerting: Template preview enhancements (#103817) API Changes: - Fixes validation in template CRUD API to be closer to how the running alertmanager will use the template. Should remove some incorrect validation errors. - Adds some missing default placeholder labels to receiver testing that are used during template testing but missing during receiver testing Template Preview: - Replaced basic preview with a readonly CodeEditor for better whitespace and alignment clarity (also adds support for future syntax highlighting in template previews for upcoming webhook payload templates) Template Selector (Receiver Form): - Refactored to use same components as Template editor for preview. - Fixed preview to work with multi-definition templates - Fixed copy to correctly copy the template contents instead of {{ template "" . }}. Template Editor: - Fixed detection of when to display functions vs snippets in multi-line expressions --- .../definitions/alertmanager_validation.go | 17 ++--- .../alertmanager_validation_test.go | 7 +-- pkg/services/ngalert/notifier/templates.go | 4 +- .../ngalert/notifier/templates_test.go | 2 +- .../ngalert/notifier/testreceivers.go | 13 ++-- .../alerting/api_notification_channel_test.go | 63 ++++++++++++++++--- .../contact-points/EditContactPoint.test.tsx | 25 ++++++-- .../components/receivers/TemplateForm.tsx | 1 + .../receivers/TemplatePreview.test.tsx | 24 +++++-- .../components/receivers/TemplatePreview.tsx | 47 ++++++++------ .../receivers/editor/autocomplete.ts | 19 +++--- .../form/fields/TemplateContentAndPreview.tsx | 45 ++++++------- .../form/fields/TemplateSelector.tsx | 4 +- .../mocks/server/handlers/alertmanagers.ts | 2 +- public/locales/en-US/grafana.json | 1 - 15 files changed, 172 insertions(+), 102 deletions(-) diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go index 72ca0ebb730..3c1b227c3e5 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go @@ -2,10 +2,8 @@ package definitions import ( "fmt" - tmplhtml "html/template" "regexp" "strings" - tmpltext "text/template" "github.com/prometheus/alertmanager/template" "gopkg.in/yaml.v3" @@ -35,17 +33,12 @@ func (t *NotificationTemplate) Validate() error { t.Template = content // Validate template contents. We try to stick as close to what will actually happen when the templates are parsed - // by the alertmanager as possible. That means parsing with both the text and html parsers and making sure we set - // the template name and options. - ttext := tmpltext.New(t.Name).Option("missingkey=zero") - ttext.Funcs(tmpltext.FuncMap(template.DefaultFuncs)) - if _, err := ttext.Parse(t.Template); err != nil { - return fmt.Errorf("invalid template: %w", err) + // by the alertmanager as possible. + tmpl, err := template.New() + if err != nil { + return fmt.Errorf("failed to create template: %w", err) } - - thtml := tmplhtml.New(t.Name).Option("missingkey=zero") - thtml.Funcs(tmplhtml.FuncMap(template.DefaultFuncs)) - if _, err := thtml.Parse(t.Template); err != nil { + if err := tmpl.Parse(strings.NewReader(t.Template)); err != nil { return fmt.Errorf("invalid template: %w", err) } diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go index 4fbbd983b13..74f186003d7 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go @@ -442,7 +442,6 @@ func TestValidateNotificationTemplates(t *testing.T) { expError: errors.New("invalid template: template: Different name than definition:1: template: multiple definition of template \"Alert Instance Template\""), }, { - // This is fine as long as the template name is different from the definition, it just ignores the extra text. name: "Extra text outside definition block - different template name and definition", template: NotificationTemplate{ Name: "Different name than definition", @@ -452,16 +451,16 @@ func TestValidateNotificationTemplates(t *testing.T) { expContent: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`, expError: nil, }, + // This test used to error because our template code parsed the template with the filename as template name. + // However, we have since moved away from this. We keep this test to ensure we don't regress. { - // This is NOT fine as the template name is the same as the definition. - // GO template parser will treat it as if it's wrapped in {{ define "Alert Instance Template" }}, thus creating a duplicate definition. name: "Extra text outside definition block - same template name and definition", template: NotificationTemplate{ Name: "Alert Instance Template", Template: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`, Provenance: "test", }, - expError: errors.New("invalid template: template: Alert Instance Template:1: template: multiple definition of template \"Alert Instance Template\""), + expContent: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`, }, } diff --git a/pkg/services/ngalert/notifier/templates.go b/pkg/services/ngalert/notifier/templates.go index 9f9a49f722c..5611bbe2b4c 100644 --- a/pkg/services/ngalert/notifier/templates.go +++ b/pkg/services/ngalert/notifier/templates.go @@ -15,8 +15,8 @@ type TestTemplatesResults = alertingNotify.TestTemplatesResults var ( DefaultLabels = map[string]string{ - prometheusModel.AlertNameLabel: `alert title`, - alertingModels.FolderTitleLabel: `folder title`, + prometheusModel.AlertNameLabel: `TestAlert`, + alertingModels.FolderTitleLabel: `Test Folder`, } DefaultAnnotations = map[string]string{ alertingModels.ValuesAnnotation: `{"B":22,"C":1}`, diff --git a/pkg/services/ngalert/notifier/templates_test.go b/pkg/services/ngalert/notifier/templates_test.go index 902c534a058..1b1da5a2890 100644 --- a/pkg/services/ngalert/notifier/templates_test.go +++ b/pkg/services/ngalert/notifier/templates_test.go @@ -84,7 +84,7 @@ CommonAnnotations: {{ range .CommonAnnotations.SortedPairs }}{{ .Name }}={{ .Val expected: TestTemplatesResults{ Results: []alertingNotify.TestTemplatesResult{{ Name: "slack.title", - Text: "\nReceiver: TestReceiver\nStatus: firing\nExternalURL: http://localhost:9093\nAlerts: 1\nFiring Alerts: 1\nResolved Alerts: 0\nGroupLabels: group_label=group_label_value \nCommonLabels: alertname=alert1 grafana_folder=folder title lbl1=val1 \nCommonAnnotations: ann1=annv1 \n", + Text: "\nReceiver: TestReceiver\nStatus: firing\nExternalURL: http://localhost:9093\nAlerts: 1\nFiring Alerts: 1\nResolved Alerts: 0\nGroupLabels: group_label=group_label_value \nCommonLabels: alertname=alert1 grafana_folder=Test Folder lbl1=val1 \nCommonAnnotations: ann1=annv1 \n", Scope: alertingNotify.TemplateScope(apimodels.RootScope), }}, Errors: nil, diff --git a/pkg/services/ngalert/notifier/testreceivers.go b/pkg/services/ngalert/notifier/testreceivers.go index 5844ff3aa30..cbd1ca29f87 100644 --- a/pkg/services/ngalert/notifier/testreceivers.go +++ b/pkg/services/ngalert/notifier/testreceivers.go @@ -5,6 +5,7 @@ import ( "encoding/json" alertingNotify "github.com/grafana/alerting/notify" + v2 "github.com/prometheus/alertmanager/api/v2" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -30,13 +31,17 @@ func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei }, }) } - var alert *alertingNotify.TestReceiversConfigAlertParams + a := &alertingNotify.PostableAlert{} if c.Alert != nil { - alert = &alertingNotify.TestReceiversConfigAlertParams{Annotations: c.Alert.Annotations, Labels: c.Alert.Labels} + a.Annotations = v2.ModelLabelSetToAPILabelSet(c.Alert.Annotations) + a.Labels = v2.ModelLabelSetToAPILabelSet(c.Alert.Labels) } - + AddDefaultLabelsAndAnnotations(a) return am.Base.TestReceivers(ctx, alertingNotify.TestReceiversConfigBodyParams{ - Alert: alert, + Alert: &alertingNotify.TestReceiversConfigAlertParams{ + Annotations: v2.APILabelSetToModelLabelSet(a.Annotations), + Labels: v2.APILabelSetToModelLabelSet(a.Labels), + }, Receivers: receivers, }) } diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index b653b3a8184..b3442b3ed6f 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -131,10 +131,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -214,10 +219,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -315,10 +325,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -392,10 +407,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -480,10 +500,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -581,10 +606,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -687,10 +717,15 @@ func TestIntegrationTestReceiversAlertCustomization(t *testing.T) { "annotations": { "annotation1": "value1", "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana", "label1": "value1" } @@ -776,10 +811,15 @@ func TestIntegrationTestReceiversAlertCustomization(t *testing.T) { "alert": { "annotations": { "summary": "This is a custom annotation", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -863,10 +903,15 @@ func TestIntegrationTestReceiversAlertCustomization(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "This is a custom label", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, diff --git a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx index 9ee0fae551b..3a7895c1a29 100644 --- a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx @@ -1,7 +1,7 @@ import 'core-js/stable/structured-clone'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { clickSelectOption } from 'test/helpers/selectOptionInTest'; -import { render, screen } from 'test/test-utils'; +import { render, screen, within } from 'test/test-utils'; import EditContactPoint from 'app/features/alerting/unified/components/contact-points/EditContactPoint'; import { AccessControlAction } from 'app/types'; @@ -15,6 +15,20 @@ const Index = () => { return
redirected
; }; +jest.mock('@grafana/ui', () => ({ + ...jest.requireActual('@grafana/ui'), + CodeEditor: function CodeEditor({ value, onBlur }: { value: string; onBlur: (newValue: string) => void }) { + return onBlur(e.currentTarget.value)} />; + }, +})); + +jest.mock( + 'react-virtualized-auto-sizer', + () => + ({ children }: { children: ({ height, width }: { height: number; width: number }) => JSX.Element }) => + children({ height: 500, width: 400 }) +); + const renderEditContactPoint = (contactPointUid: string) => render( @@ -30,8 +44,7 @@ beforeEach(() => { grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite]); }); -const getTemplatePreviewContent = async () => - await screen.findByRole('presentation', { description: /Preview with the default payload/i }); +const getTemplatePreviewContent = async () => within(screen.getByTestId('template-preview')).getByTestId('mockeditor'); const templatesSelectorTestId = 'existing-templates-selector'; @@ -44,11 +57,11 @@ describe('Edit contact point', () => { 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); + expect(await getTemplatePreviewContent()).toHaveValue(`some example preview for {{ template "slack-template" . }}`); // 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); + expect(await getTemplatePreviewContent()).toHaveValue(`some example preview for {{ template "custom-email" . }}`); // Close the drawer await user.click(screen.getByRole('button', { name: /^save$/i })); @@ -62,7 +75,7 @@ describe('Edit contact point', () => { await user.click(screen.getByRole('radio', { name: /select notification template/i })); await clickSelectOption(screen.getByTestId(templatesSelectorTestId), 'slack-template'); - expect(await getTemplatePreviewContent()).toHaveTextContent(/some example preview for slack-template/i); + expect(await getTemplatePreviewContent()).toHaveValue(`some example preview for {{ template "slack-template" . }}`); // Close the drawer await user.click(screen.getByRole('button', { name: /^save$/i })); diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index 4728dbdb312..bf2f7167bc6 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -336,6 +336,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) ({ + ...jest.requireActual('@grafana/ui'), + CodeEditor: function CodeEditor({ value, onBlur }: { value: string; onBlur: (newValue: string) => void }) { + return onBlur(e.currentTarget.value)} />; + }, +})); + jest.mock( 'react-virtualized-auto-sizer', () => @@ -50,6 +57,7 @@ describe('TemplatePreview component', () => { , @@ -66,6 +74,7 @@ describe('TemplatePreview component', () => { , @@ -81,6 +90,7 @@ describe('TemplatePreview component', () => { , @@ -100,6 +110,7 @@ describe('TemplatePreview component', () => { , @@ -123,6 +134,7 @@ describe('TemplatePreview component', () => { , @@ -133,8 +145,11 @@ describe('TemplatePreview component', () => { await waitFor(() => { expect(previews()).toHaveLength(2); }); - expect(previews()[0]).toHaveTextContent('This is the template result bla bla bla'); - expect(previews()[1]).toHaveTextContent('This is the template2 result bla bla bla'); + const previewItems = previews(); + expect(within(previewItems[0]).getByRole('banner')).toHaveTextContent('template1'); + expect(within(previewItems[0]).getByTestId('mockeditor')).toHaveValue('This is the template result bla bla bla'); + expect(within(previewItems[1]).getByRole('banner')).toHaveTextContent('template2'); + expect(within(previewItems[1]).getByTestId('mockeditor')).toHaveValue('This is the template2 result bla bla bla'); }); it('Should render preview response with some errors, if payload has correct format ', async () => { @@ -151,6 +166,7 @@ describe('TemplatePreview component', () => { , @@ -165,6 +181,6 @@ describe('TemplatePreview component', () => { expect(alerts()[1]).toHaveTextContent(/Unexpected "{" in operand/i); const previewContent = screen.getByRole('listitem'); - expect(previewContent).toHaveTextContent('This is the template result bla bla bla'); + expect(within(previewContent).getByTestId('mockeditor')).toHaveValue('This is the template result bla bla bla'); }); }); diff --git a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx index 179d01c3588..6f743447ff1 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx @@ -1,39 +1,35 @@ import { css, cx } from '@emotion/css'; import { compact, uniqueId } from 'lodash'; import * as React from 'react'; -import { useFormContext } from 'react-hook-form'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; -import { Alert, Box, Button, useStyles2 } from '@grafana/ui'; +import { Alert, Box, Button, CodeEditor, useStyles2 } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; import { TemplatePreviewErrors, TemplatePreviewResponse, TemplatePreviewResult } from '../../api/templateApi'; import { stringifyErrorLike } from '../../utils/misc'; import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader'; -import type { TemplateFormValues } from './TemplateForm'; import { usePreviewTemplate } from './usePreviewTemplate'; export function TemplatePreview({ payload, templateName, + templateContent, payloadFormatError, setPayloadFormatError, className, }: { payload: string; templateName: string; + templateContent: string; payloadFormatError: string | null; setPayloadFormatError: (value: React.SetStateAction) => void; className?: string; }) { const styles = useStyles2(getStyles); - const { watch } = useFormContext(); - - const templateContent = watch('content'); - const { data, isLoading, @@ -74,13 +70,25 @@ function PreviewResultViewer({ previews }: { previews: TemplatePreviewResult[] } const singleTemplate = previews.length === 1; return ( -
    - {previews.map((preview) => ( -
  • - {singleTemplate ? null :
    {preview.name}
    } -
    {preview.text ?? ''}
    -
  • - ))} +
      + {previews.map((preview) => { + return ( +
    • + {singleTemplate ? null :
      {preview.name}
      } + +
    • + ); + })}
    ); } @@ -101,6 +109,11 @@ const getStyles = (theme: GrafanaTheme2) => ({ borderRadius: theme.shape.radius.default, border: `1px solid ${theme.colors.border.medium}`, }), + editorContainer: css({ + width: '100%', + height: '100%', + border: 'none', + }), viewerContainer: ({ height }: { height: number }) => css({ height, @@ -128,12 +141,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ errorText: css({ color: theme.colors.error.text, }), - pre: css({ - backgroundColor: 'transparent', - margin: 0, - border: 'none', - padding: theme.spacing(2), - }), }, }); diff --git a/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts b/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts index edd248d5c52..29439cb5a5f 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts @@ -49,17 +49,16 @@ export function registerGoTemplateAutocomplete(monaco: Monaco): IDisposable { } function isInsideGoExpression(model: editor.ITextModel, position: Position) { - const searchRange = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: model.getLineMinColumn(position.lineNumber), - endColumn: model.getLineMaxColumn(position.lineNumber), - }; + // Need to trick findMatches into enabling multiline matches. One way to do this is to have \n in the regex. + const goSyntaxRegex = '\\{\\{(?:.|\\n)+?\\}\\}'; + const matches = model.findMatches(goSyntaxRegex, model.getFullModelRange(), true, false, null, false); - const goSyntaxRegex = '\\{\\{[a-zA-Z0-9._() "]+\\}\\}'; - const matches = model.findMatches(goSyntaxRegex, searchRange, true, false, null, true); - - return matches.some((match) => match.range.containsPosition(position)); + return matches.some((match) => + match.range.containsPosition({ + lineNumber: position.lineNumber, + column: position.column + 1, // Stricter check to avoid matching on the closing bracket. + }) + ); } export class CompletionProvider { diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateContentAndPreview.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateContentAndPreview.tsx index 08f6a107ab3..e5d06e8a7b7 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateContentAndPreview.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateContentAndPreview.tsx @@ -10,8 +10,9 @@ import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/d import { EditorColumnHeader } from '../../../contact-points/templates/EditorColumnHeader'; import { TemplateEditor } from '../../TemplateEditor'; -import { getPreviewResults } from '../../TemplatePreview'; -import { usePreviewTemplate } from '../../usePreviewTemplate'; +import { TemplatePreview } from '../../TemplatePreview'; + +import { getUseTemplateText } from './utils'; export function TemplateContentAndPreview({ payload, @@ -33,11 +34,6 @@ export function TemplateContentAndPreview({ const { selectedAlertmanager } = useAlertmanager(); const isGrafanaAlertManager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME; - const { data, error } = usePreviewTemplate(templateContent, templateName, payload, setPayloadFormatError); - const previewToRender = getPreviewResults(error, payloadFormatError, data); - - const templatePreviewId = 'template-preview'; - return (
    @@ -62,24 +58,15 @@ export function TemplateContentAndPreview({
    {isGrafanaAlertManager && ( -
    - - -
    - {previewToRender} -
    -
    -
    + )}
    ); @@ -98,6 +85,14 @@ const getStyles = (theme: GrafanaTheme2) => ({ borderRadius: theme.shape.radius.default, border: `1px solid ${theme.colors.border.medium}`, }), + templatePreview: css({ + flex: 1, + display: 'flex', + }), + minEditorSize: css({ + minHeight: 300, + minWidth: 300, + }), viewerContainer: ({ height }: { height: number | string }) => css({ height, diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx index dcd2516ebb4..5260695f6c1 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx @@ -220,9 +220,7 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe /> - copyToClipboard(getUseTemplateText(template?.value?.name ?? defaultTemplateValue?.value?.name ?? '')) - } + onClick={() => copyToClipboard(template?.value?.content ?? defaultTemplateValue?.value?.content ?? '')} name="copy" /> diff --git a/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts b/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts index b95fc2dcaea..c02659dcab5 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts @@ -142,7 +142,7 @@ const getGrafanaAlertmanagerTemplatePreview = () => 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({ results: [{ name: 'asdasd', text: `some example preview for ${body.template}` }] }); } return HttpResponse.json({}); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c318b56e95a..e7633b9d8a3 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1963,7 +1963,6 @@ "tooltip-delete": "delete" }, "template-content-and-preview": { - "label-preview-with-the-default-payload": "Preview with the default payload", "label-template-content": "Template content" }, "template-data-table": {