diff --git a/public/app/features/alerting/unified/components/common/TextVariants.tsx b/public/app/features/alerting/unified/components/common/TextVariants.tsx index 3842c2b8287..1df419b883d 100644 --- a/public/app/features/alerting/unified/components/common/TextVariants.tsx +++ b/public/app/features/alerting/unified/components/common/TextVariants.tsx @@ -6,3 +6,4 @@ import { Text } from '@grafana/ui'; export const PrimaryText = ({ content }: { content: string }) => {content}; +export const CodeText = ({ content }: { content: string }) => {content}; diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index fa70dacd4ab..dea25168694 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -126,6 +126,13 @@ describe('contact points', () => { }); }); + describe('templates tab', () => { + it('shows a warning when a template is misconfigured', async () => { + renderWithProvider(, { initialEntries: ['/?tab=templates'] }); + expect((await screen.findAllByText(/^misconfigured$/i))[0]).toBeInTheDocument(); + }); + }); + it('should show / hide loading states, have all actions enabled', async () => { renderWithProvider(); diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json index 784c8ca555f..0077f89f3a0 100644 --- a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json @@ -3,10 +3,13 @@ "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 with spaces": "{{ define \"template with spaces\" }} Custom template with spaces in the name {{ end }}", + "misconfigured-template": "{{ define \"misconfigured template\" }} Template that is defined in template_files but not templates {{ end }}", + "misconfigured and provisioned": "{{ define \"misconfigured and provisioned template\" }} Provisioned template that is defined in template_files but not templates {{ end }}" }, "template_file_provenances": { - "provisioned-template": "api" + "provisioned-template": "api", + "misconfigured and provisioned": "api" }, "alertmanager_config": { "route": { diff --git a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts index e98aca41847..f1d15f6dfe5 100644 --- a/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts +++ b/public/app/features/alerting/unified/components/contact-points/useNotificationTemplates.ts @@ -26,6 +26,7 @@ export interface NotificationTemplate { title: string; content: string; provenance: string; + missing?: boolean; } const { useGetAlertmanagerConfigurationQuery, useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi; @@ -84,12 +85,15 @@ function templateGroupToTemplate( } function amConfigToTemplates(config: AlertManagerCortexConfig): NotificationTemplate[] { + const { alertmanager_config } = config; + const { templates = [] } = alertmanager_config; return Object.entries(config.template_files).map(([title, content]) => ({ uid: title, title, content, // Undefined, null or empty string should be converted to PROVENANCE_NONE provenance: (config.template_file_provenances ?? {})[title] || PROVENANCE_NONE, + missing: !templates.includes(title), })); } diff --git a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx index a7d1df7ffb5..93a0114bc17 100644 --- a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx @@ -97,7 +97,11 @@ describe('alerting API server disabled', () => { ); const testBody = await testRequest?.json(); - const saveBody = await saveRequest?.json(); + const fullSaveBody = await saveRequest?.json(); + + // Only snapshot and check the receivers, as we don't want other tests to break this + // just because we added something new to the mock config + const saveBody = fullSaveBody.alertmanager_config.receivers; expect([testBody]).toMatchSnapshot(); expect([saveBody]).toMatchSnapshot(); diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx index 240997db6b7..c644a3b5fb7 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx @@ -1,8 +1,10 @@ import { Fragment, useState } from 'react'; import { logError } from '@grafana/runtime'; -import { ConfirmModal, useStyles2 } from '@grafana/ui'; +import { Badge, ConfirmModal, Tooltip, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; +import { t, Trans } from 'app/core/internationalization'; +import { CodeText } from 'app/features/alerting/unified/components/common/TextVariants'; import { Authorize } from '../../components/Authorize'; import { AlertmanagerAction } from '../../hooks/useAbilities'; @@ -116,8 +118,8 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic const [isExpanded, setIsExpanded] = useState(false); const { isProvisioned } = useNotificationTemplateMetadata(notificationTemplate); - const { uid, title: name, content: template } = notificationTemplate; - + const { uid, title: name, content: template, missing } = notificationTemplate; + const misconfiguredBadgeText = t('alerting.templates.misconfigured-badge-text', 'Misconfigured'); return ( @@ -125,7 +127,25 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic setIsExpanded(!isExpanded)} /> - {name} {isProvisioned && } + {name} {isProvisioned && }{' '} + {missing && ( + + This template is misconfigured. +
+ + Templates must be defined in both the and{' '} + sections of your alertmanager configuration. + + + } + > + + + +
+ )} {isProvisioned && ( diff --git a/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap b/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap index 681e1ee181f..ededb25feb5 100644 --- a/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap +++ b/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap @@ -34,142 +34,113 @@ exports[`alerting API server disabled should be able to test and save a receiver exports[`alerting API server disabled should be able to test and save a receiver 2`] = ` [ - { - "alertmanager_config": { - "mute_time_intervals": [], - "receivers": [ + [ + { + "grafana_managed_receiver_configs": [ { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "grafana-default-email", - "secureFields": {}, - "settings": { - "addresses": "gilles.demey@grafana.com", - "singleEmail": false, - }, - "type": "email", - "uid": "xeKQrBrnk", - }, - ], + "disableResolveMessage": false, "name": "grafana-default-email", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "provisioned-contact-point", - "provenance": "api", - "secureFields": {}, - "settings": { - "addresses": "gilles.demey@grafana.com", - "singleEmail": false, - }, - "type": "email", - "uid": "s8SdCVjnk", - }, - ], - "name": "provisioned-contact-point", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "lotsa-emails", - "secureFields": {}, - "settings": { - "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", - "singleEmail": false, - }, - "type": "email", - "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", - }, - ], - "name": "lotsa-emails", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "Slack with multiple channels", - "secureFields": { - "token": true, - }, - "settings": { - "recipient": "test-alerts", - }, - "type": "slack", - "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", - }, - { - "disableResolveMessage": false, - "name": "Slack with multiple channels", - "secureFields": { - "token": true, - }, - "settings": { - "recipient": "test-alerts2", - }, - "type": "slack", - "uid": "b286a3be-f690-49e2-8605-b075cbace2df", - }, - ], - "name": "Slack with multiple channels", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "Oncall-integration", - "settings": { - "url": "https://oncall-endpoint.example.com", - }, - "type": "oncall", - }, - ], - "name": "OnCall Conctact point", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "my new receiver", - "secureSettings": {}, - "settings": { - "addresses": "tester@grafana.com", - "singleEmail": false, - }, - "type": "email", - }, - ], - "name": "my new receiver", - }, - ], - "route": { - "receiver": "grafana-default-email", - "routes": [ - { - "receiver": "provisioned-contact-point", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey@grafana.com", + "singleEmail": false, }, - ], - }, - "templates": [ - "slack-template", - "custom-email", - "provisioned-template", - "template with spaces", + "type": "email", + "uid": "xeKQrBrnk", + }, ], - "time_intervals": [], + "name": "grafana-default-email", }, - "template_file_provenances": { - "provisioned-template": "api", + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "provisioned-contact-point", + "provenance": "api", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "s8SdCVjnk", + }, + ], + "name": "provisioned-contact-point", }, - "template_files": { - "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 }}", + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "lotsa-emails", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", + }, + ], + "name": "lotsa-emails", }, - }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "Slack with multiple channels", + "secureFields": { + "token": true, + }, + "settings": { + "recipient": "test-alerts", + }, + "type": "slack", + "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", + }, + { + "disableResolveMessage": false, + "name": "Slack with multiple channels", + "secureFields": { + "token": true, + }, + "settings": { + "recipient": "test-alerts2", + }, + "type": "slack", + "uid": "b286a3be-f690-49e2-8605-b075cbace2df", + }, + ], + "name": "Slack with multiple channels", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "Oncall-integration", + "settings": { + "url": "https://oncall-endpoint.example.com", + }, + "type": "oncall", + }, + ], + "name": "OnCall Conctact point", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "my new receiver", + "secureSettings": {}, + "settings": { + "addresses": "tester@grafana.com", + "singleEmail": false, + }, + "type": "email", + }, + ], + "name": "my new receiver", + }, + ], ] `; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx index 73b8d455436..03266950cac 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react'; import { render, screen, userEvent } from 'test/test-utils'; import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types'; +import alertmanagerConfigMock from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; @@ -98,7 +99,7 @@ describe('TemplatesPicker', () => { const input = screen.getByRole('combobox'); expect(screen.queryByText('slack-template')).not.toBeInTheDocument(); await userEvent.click(input); - expect(screen.getAllByRole('option')).toHaveLength(7); // 4 templates in mock plus 3 in the default template + expect(screen.getAllByRole('option')).toHaveLength(Object.keys(alertmanagerConfigMock.template_files).length + 3); // 4 templates in mock plus 3 in the default template const template = screen.getByRole('option', { name: 'slack-template' }); await userEvent.click(template); expect(screen.getByText('slack-template')).toBeInTheDocument(); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index bd56d8aff4e..09aeebf2b0d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -284,6 +284,11 @@ "ofQuery": { "To": "TO" } + }, + "templates": { + "misconfigured-badge-text": "Misconfigured", + "misconfigured-warning": "This template is misconfigured.", + "misconfigured-warning-details": "Templates must be defined in both the <1> and <4> sections of your alertmanager configuration." } }, "annotations": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index a6f27524603..281d8fd3cbf 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -284,6 +284,11 @@ "ofQuery": { "To": "ŦØ" } + }, + "templates": { + "misconfigured-badge-text": "Mįşčőʼnƒįģūřęđ", + "misconfigured-warning": "Ŧĥįş ŧęmpľäŧę įş mįşčőʼnƒįģūřęđ.", + "misconfigured-warning-details": "Ŧęmpľäŧęş mūşŧ þę đęƒįʼnęđ įʼn þőŧĥ ŧĥę <1> äʼnđ <4> şęčŧįőʼnş őƒ yőūř äľęřŧmäʼnäģęř čőʼnƒįģūřäŧįőʼn." } }, "annotations": {