Alerting: Fix notification templates layout (#101232)

This commit is contained in:
Gilles De Mey
2025-02-26 11:11:49 +01:00
committed by GitHub
parent 9406830a94
commit 6aa353cde6
7 changed files with 264 additions and 182 deletions
@@ -1,6 +1,5 @@
import { Route, Routes } from 'react-router-dom-v5-compat';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import DuplicateMessageTemplate from './components/contact-points/DuplicateMessageTemplate';
import EditMessageTemplate from './components/contact-points/EditMessageTemplate';
import NewMessageTemplate from './components/contact-points/NewMessageTemplate';
@@ -8,21 +7,11 @@ import { withPageErrorBoundary } from './withPageErrorBoundary';
function NotificationTemplates() {
return (
<AlertmanagerPageWrapper
navId="receivers"
accessType="notification"
pageNav={{
id: 'templates',
text: 'Notification templates',
subTitle: 'Create and edit a group of notification templates',
}}
>
<Routes>
<Route path=":name/edit" element={<EditMessageTemplate />} />
<Route path="new" element={<NewMessageTemplate />} />
<Route path=":name/duplicate" element={<DuplicateMessageTemplate />} />
</Routes>
</AlertmanagerPageWrapper>
<Routes>
<Route path="new" element={<NewMessageTemplate />} />
<Route path=":name/edit" element={<EditMessageTemplate />} />
<Route path=":name/duplicate" element={<DuplicateMessageTemplate />} />
</Routes>
);
}
@@ -2,16 +2,19 @@ import { useParams } from 'react-router-dom-v5-compat';
import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
import { t } from 'app/core/internationalization';
import { isNotFoundError } from '../../api/util';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { generateCopiedName } from '../../utils/duplicate';
import { stringifyErrorLike } from '../../utils/misc';
import { updateDefinesWithUniqueValue } from '../../utils/templates';
import { createRelativeUrl } from '../../utils/url';
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
import { TemplateForm } from '../receivers/TemplateForm';
import { ActiveTab } from './ContactPoints';
import { useGetNotificationTemplate, useNotificationTemplates } from './useNotificationTemplates';
const notFoundComponent = <EntityNotFound entity="Notification template" />;
@@ -23,16 +26,19 @@ const DuplicateMessageTemplateComponent = () => {
const {
currentData: template,
isLoading,
error,
isLoading: isLoadingTemplate,
error: templateFetchError,
} = useGetNotificationTemplate({ alertmanager: selectedAlertmanager ?? '', uid: templateUid ?? '' });
const {
currentData: templates,
isLoading: templatesLoading,
error: templatesError,
error: templatesFetchError,
} = useNotificationTemplates({ alertmanager: selectedAlertmanager ?? '' });
const isLoading = isLoadingTemplate || templatesLoading;
const error = templateFetchError || templatesFetchError;
if (!selectedAlertmanager) {
return <EntityNotFound entity="Alertmanager" />;
}
@@ -41,11 +47,11 @@ const DuplicateMessageTemplateComponent = () => {
return <EntityNotFound entity="Notification template" />;
}
if (isLoading || templatesLoading) {
if (isLoading) {
return <LoadingPlaceholder text="Loading notification template" />;
}
if (error || templatesError || !template || !templates) {
if (error) {
return isNotFoundError(error) ? (
notFoundComponent
) : (
@@ -55,6 +61,10 @@ const DuplicateMessageTemplateComponent = () => {
);
}
if (!template) {
return notFoundComponent;
}
const duplicatedName = generateCopiedName(template.title, templates?.map((t) => t.title) ?? []);
return (
@@ -67,7 +77,24 @@ const DuplicateMessageTemplateComponent = () => {
function DuplicateMessageTemplate() {
return (
<AlertmanagerPageWrapper navId="receivers" accessType="notification">
<AlertmanagerPageWrapper
navId="receivers"
accessType="notification"
pageNav={{
id: 'templates',
text: t('alerting.notification-templates.duplicate.title', 'Duplicate notification template group'),
subTitle: t(
'alerting.notification-templates.duplicate.subTitle',
'Duplicate a group of notification templates'
),
parentItem: {
text: t('alerting.common.titles.notification-templates', 'Notification Templates'),
url: createRelativeUrl('/alerting/notifications', {
tab: ActiveTab.NotificationTemplates,
}),
},
}}
>
<DuplicateMessageTemplateComponent />
</AlertmanagerPageWrapper>
);
@@ -2,14 +2,17 @@ import { useParams } from 'react-router-dom-v5-compat';
import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
import { t } from 'app/core/internationalization';
import { isNotFoundError } from '../../api/util';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { stringifyErrorLike } from '../../utils/misc';
import { createRelativeUrl } from '../../utils/url';
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
import { TemplateForm } from '../receivers/TemplateForm';
import { ActiveTab } from './ContactPoints';
import { useGetNotificationTemplate } from './useNotificationTemplates';
const notFoundComponent = <EntityNotFound entity="Notification template" />;
@@ -19,7 +22,7 @@ const EditMessageTemplateComponent = () => {
const templateUid = name ? decodeURIComponent(name) : undefined;
const { selectedAlertmanager } = useAlertmanager();
const { currentData, isLoading, error } = useGetNotificationTemplate({
const { currentData, isLoading, error, isUninitialized } = useGetNotificationTemplate({
alertmanager: selectedAlertmanager ?? '',
uid: templateUid ?? '',
});
@@ -28,7 +31,7 @@ const EditMessageTemplateComponent = () => {
return <EntityNotFound entity="Notification template" />;
}
if (isLoading) {
if (isLoading || isUninitialized) {
return <LoadingPlaceholder text="Loading template..." />;
}
@@ -51,7 +54,21 @@ const EditMessageTemplateComponent = () => {
function EditMessageTemplate() {
return (
<AlertmanagerPageWrapper navId="receivers" accessType="notification">
<AlertmanagerPageWrapper
navId="receivers"
accessType="notification"
pageNav={{
id: 'templates',
text: t('alerting.notification-templates.edit.title', 'Edit notification template group'),
subTitle: t('alerting.notification-templates.edit.subTitle', 'Edit a group of notification templates'),
parentItem: {
text: t('alerting.common.titles.notification-templates', 'Notification Templates'),
url: createRelativeUrl('/alerting/notifications', {
tab: ActiveTab.NotificationTemplates,
}),
},
}}
>
<EditMessageTemplateComponent />
</AlertmanagerPageWrapper>
);
@@ -1,16 +1,38 @@
import { t } from 'app/core/internationalization';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { createRelativeUrl } from '../../utils/url';
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
import { TemplateForm } from '../receivers/TemplateForm';
function NewMessageTemplate() {
const { selectedAlertmanager } = useAlertmanager();
import { ActiveTab } from './ContactPoints';
function NewMessageTemplatePage() {
return (
<AlertmanagerPageWrapper navId="receivers" accessType="notification">
<TemplateForm alertmanager={selectedAlertmanager ?? ''} />
<AlertmanagerPageWrapper
navId="receivers"
accessType="notification"
pageNav={{
id: 'templates',
text: t('alerting.notification-templates.new.title', 'New notification template group'),
subTitle: t('alerting.notification-templates.new.subTitle', 'Create a new group of notification templates'),
parentItem: {
text: t('alerting.common.titles.notification-templates', 'Notification Templates'),
url: createRelativeUrl('/alerting/notifications', {
tab: ActiveTab.NotificationTemplates,
}),
},
}}
>
<NewMessageTemplate />
</AlertmanagerPageWrapper>
);
}
export default withPageErrorBoundary(NewMessageTemplate);
function NewMessageTemplate() {
const { selectedAlertmanager } = useAlertmanager();
return <TemplateForm alertmanager={selectedAlertmanager ?? ''} />;
}
export default withPageErrorBoundary(NewMessageTemplatePage);
@@ -25,17 +25,14 @@ import {
useStyles2,
} from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { useCleanup } from 'app/core/hooks/useCleanup';
import { Trans, t } from 'app/core/internationalization';
import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints';
import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types';
import { AppChromeUpdate } from '../../../../../core/components/AppChrome/AppChromeUpdate';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { makeAMLink, stringifyErrorLike } from '../../utils/misc';
import { initialAsyncRequestState } from '../../utils/redux';
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
import { Spacer } from '../Spacer';
import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader';
import {
NotificationTemplate,
@@ -95,15 +92,14 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
const appNotification = useAppNotification();
const [createNewTemplate] = useCreateNotificationTemplate({ alertmanager });
const [updateTemplate] = useUpdateNotificationTemplate({ alertmanager });
const [createNewTemplate, { error: createTemplateError }] = useCreateNotificationTemplate({ alertmanager });
const [updateTemplate, { error: updateTemplateError }] = useUpdateNotificationTemplate({ alertmanager });
const { titleIsUnique } = useValidateNotificationTemplate({ alertmanager, originalTemplate });
useCleanup((state) => (state.unifiedAlerting.saveAMConfig = initialAsyncRequestState));
const formRef = useRef<HTMLFormElement>(null);
const isGrafanaAlertManager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
const { error } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
const error = updateTemplateError ?? createTemplateError;
const [cheatsheetOpened, toggleCheatsheetOpened] = useToggle(false);
@@ -168,28 +164,9 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
setValue('content', newValue);
};
const actionButtons = (
<Stack>
<Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={isSubmitting}>
<Trans i18nKey="common.save">Save</Trans>
</Button>
<LinkButton
disabled={isSubmitting}
href={makeAMLink('alerting/notifications', alertmanager, {
tab: ContactPointsActiveTabs.NotificationTemplates,
})}
variant="secondary"
size="sm"
>
<Trans i18nKey="common.cancel">Cancel</Trans>
</LinkButton>
</Stack>
);
return (
<>
<FormProvider {...formApi}>
<AppChromeUpdate actions={actionButtons} />
<form onSubmit={handleSubmit(submit)} ref={formRef} className={styles.form} aria-label="Template form">
{/* error message */}
{error && (
@@ -206,136 +183,155 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
{/* name field for the template */}
<FieldSet disabled={isProvisioned} className={styles.fieldset}>
<InlineField
label="Template group name"
error={errors?.title?.message}
invalid={!!errors.title?.message}
required
className={styles.nameField}
>
<Input
{...register('title', {
required: { value: true, message: 'Required.' },
validate: { titleIsUnique },
})}
placeholder="Give your template group a name"
width={42}
autoFocus={true}
id="new-template-name"
/>
</InlineField>
<Stack direction="column" gap={1} alignItems="stretch" minHeight="100%">
{/* name and save buttons */}
<Stack direction="row" alignItems="center">
<InlineField
label="Template group name"
error={errors?.title?.message}
invalid={!!errors.title?.message}
required
>
<Input
{...register('title', {
required: { value: true, message: 'Required.' },
validate: { titleIsUnique },
})}
placeholder="Give your template group a name"
width={42}
autoFocus={true}
id="new-template-name"
/>
</InlineField>
<Spacer />
<Stack>
<Button onClick={() => formRef.current?.requestSubmit()} variant="primary" disabled={isSubmitting}>
<Trans i18nKey="common.save">Save</Trans>
</Button>
<LinkButton
disabled={isSubmitting}
href={makeAMLink('alerting/notifications', alertmanager, {
tab: ContactPointsActiveTabs.NotificationTemplates,
})}
variant="secondary"
>
<Trans i18nKey="common.cancel">Cancel</Trans>
</LinkButton>
</Stack>
</Stack>
{/* editor layout */}
<div {...rowSplitter.containerProps} className={styles.contentContainer}>
<div {...rowSplitter.primaryProps}>
{/* template content and payload editor column – full height and half-width */}
<div {...columnSplitter.containerProps} className={styles.contentField}>
{/* template editor */}
<div {...columnSplitter.primaryProps}>
{/* primaryProps will set "minHeight: min-content;" so we have to make sure to apply minHeight to the child */}
<div className={cx(styles.flexColumn, styles.containerWithBorderAndRadius, styles.minEditorSize)}>
<div>
<EditorColumnHeader
label="Template group"
actions={
<>
{/* examples dropdown – only available for Grafana Alertmanager */}
{isGrafanaAlertManager && (
<Dropdown
overlay={
<Menu>
{GlobalTemplateDataExamples.map((item, index) => (
{/* editor layout */}
<div {...rowSplitter.containerProps} className={styles.contentContainer}>
<div {...rowSplitter.primaryProps}>
{/* template content and payload editor column – full height and half-width */}
<div {...columnSplitter.containerProps} className={styles.contentField}>
{/* template editor */}
<div {...columnSplitter.primaryProps}>
{/* primaryProps will set "minHeight: min-content;" so we have to make sure to apply minHeight to the child */}
<div className={cx(styles.flexColumn, styles.containerWithBorderAndRadius, styles.minEditorSize)}>
<div>
<EditorColumnHeader
label="Template group"
actions={
<>
{/* examples dropdown – only available for Grafana Alertmanager */}
{isGrafanaAlertManager && (
<Dropdown
overlay={
<Menu>
{GlobalTemplateDataExamples.map((item, index) => (
<Menu.Item
key={index}
label={item.description}
onClick={() => appendExample(item.example)}
/>
))}
<Menu.Divider />
<Menu.Item
key={index}
label={item.description}
onClick={() => appendExample(item.example)}
label={'Examples documentation'}
url="https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/examples/"
target="_blank"
icon="external-link-alt"
/>
))}
<Menu.Divider />
<Menu.Item
label={'Examples documentation'}
url="https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/examples/"
target="_blank"
icon="external-link-alt"
/>
</Menu>
}
</Menu>
}
>
<Button variant="secondary" size="sm" icon="angle-down">
<Trans i18nKey="alerting.templates.editor.add-example">Add example</Trans>
</Button>
</Dropdown>
)}
<Button
icon="question-circle"
size="sm"
fill="outline"
variant="secondary"
onClick={toggleCheatsheetOpened}
>
<Button variant="secondary" size="sm" icon="angle-down">
<Trans i18nKey="alerting.templates.editor.add-example">Add example</Trans>
</Button>
</Dropdown>
)}
<Button
icon="question-circle"
size="sm"
fill="outline"
variant="secondary"
onClick={toggleCheatsheetOpened}
>
<Trans i18nKey="common.help">Help</Trans>
</Button>
</>
}
/>
</div>
<Box flex={1}>
<AutoSizer>
{({ width, height }) => (
<TemplateEditor
value={getValues('content')}
onBlur={(value) => setValue('content', value)}
containerStyles={styles.editorContainer}
width={width}
height={height}
/>
)}
</AutoSizer>
</Box>
</div>
</div>
{/* payload editor – only available for Grafana Alertmanager */}
{isGrafanaAlertManager && (
<>
<div {...columnSplitter.splitterProps} />
<div {...columnSplitter.secondaryProps}>
<div
className={cx(
styles.containerWithBorderAndRadius,
styles.minEditorSize,
styles.payloadEditor,
styles.flexFull
)}
>
<PayloadEditor
payload={payload}
defaultPayload={defaultPayloadString}
setPayload={setPayload}
setPayloadFormatError={setPayloadFormatError}
payloadFormatError={payloadFormatError}
<Trans i18nKey="common.help">Help</Trans>
</Button>
</>
}
/>
</div>
<Box flex={1}>
<AutoSizer>
{({ width, height }) => (
<TemplateEditor
value={getValues('content')}
onBlur={(value) => setValue('content', value)}
containerStyles={styles.editorContainer}
width={width}
height={height}
/>
)}
</AutoSizer>
</Box>
</div>
</>
)}
</div>
</div>
{/* preview column – full height and half-width */}
{isGrafanaAlertManager && (
<>
<div {...rowSplitter.secondaryProps}>
<div {...rowSplitter.splitterProps} />
<TemplatePreview
payload={payload}
templateName={watch('title')}
setPayloadFormatError={setPayloadFormatError}
payloadFormatError={payloadFormatError}
className={cx(styles.templatePreview, styles.minEditorSize)}
/>
</div>
{/* payload editor – only available for Grafana Alertmanager */}
{isGrafanaAlertManager && (
<>
<div {...columnSplitter.splitterProps} />
<div {...columnSplitter.secondaryProps}>
<div
className={cx(
styles.containerWithBorderAndRadius,
styles.minEditorSize,
styles.payloadEditor,
styles.flexFull
)}
>
<PayloadEditor
payload={payload}
defaultPayload={defaultPayloadString}
setPayload={setPayload}
setPayloadFormatError={setPayloadFormatError}
payloadFormatError={payloadFormatError}
/>
</div>
</div>
</>
)}
</div>
</>
)}
</div>
</div>
{/* preview column – full height and half-width */}
{isGrafanaAlertManager && (
<>
<div {...rowSplitter.secondaryProps}>
<div {...rowSplitter.splitterProps} />
<TemplatePreview
payload={payload}
templateName={watch('title')}
setPayloadFormatError={setPayloadFormatError}
payloadFormatError={payloadFormatError}
className={cx(styles.templatePreview, styles.minEditorSize)}
/>
</div>
</>
)}
</div>
</Stack>
</FieldSet>
</form>
</FormProvider>
@@ -439,9 +435,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
label: css({
margin: 0,
}),
nameField: css({
marginBottom: theme.spacing(1),
}),
contentContainer: css({
flex: 1,
display: 'flex',
+17
View File
@@ -295,6 +295,9 @@
"export-all": "Export all",
"loading": "Loading...",
"search-by-matchers": "Search by matchers",
"titles": {
"notification-templates": "Notification Templates"
},
"view": "View"
},
"contact-points": {
@@ -404,6 +407,20 @@
"title": "Alert instance routing preview",
"uninitialized": "When you have your folder selected and your query and labels are configured, click \"Preview routing\" to see the results here."
},
"notification-templates": {
"duplicate": {
"subTitle": "Duplicate a group of notification templates",
"title": "Duplicate notification template group"
},
"edit": {
"subTitle": "Edit a group of notification templates",
"title": "Edit notification template group"
},
"new": {
"subTitle": "Create a new group of notification templates",
"title": "New notification template group"
}
},
"policies": {
"default-policy": {
"description": "All alert instances will be handled by the default policy if no other matching policies are found.",
+17
View File
@@ -295,6 +295,9 @@
"export-all": "Ēχpőřŧ äľľ",
"loading": "Ŀőäđįʼnģ...",
"search-by-matchers": "Ŝęäřčĥ þy mäŧčĥęřş",
"titles": {
"notification-templates": "Ńőŧįƒįčäŧįőʼn Ŧęmpľäŧęş"
},
"view": "Vįęŵ"
},
"contact-points": {
@@ -404,6 +407,20 @@
"title": "Åľęřŧ įʼnşŧäʼnčę řőūŧįʼnģ přęvįęŵ",
"uninitialized": "Ŵĥęʼn yőū ĥävę yőūř ƒőľđęř şęľęčŧęđ äʼnđ yőūř qūęřy äʼnđ ľäþęľş äřę čőʼnƒįģūřęđ, čľįčĸ \"Přęvįęŵ řőūŧįʼnģ\" ŧő şęę ŧĥę řęşūľŧş ĥęřę."
},
"notification-templates": {
"duplicate": {
"subTitle": "Đūpľįčäŧę ä ģřőūp őƒ ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş",
"title": "Đūpľįčäŧę ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧę ģřőūp"
},
"edit": {
"subTitle": "Ēđįŧ ä ģřőūp őƒ ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş",
"title": "Ēđįŧ ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧę ģřőūp"
},
"new": {
"subTitle": "Cřęäŧę ä ʼnęŵ ģřőūp őƒ ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş",
"title": "Ńęŵ ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧę ģřőūp"
}
},
"policies": {
"default-policy": {
"description": "Åľľ äľęřŧ įʼnşŧäʼnčęş ŵįľľ þę ĥäʼnđľęđ þy ŧĥę đęƒäūľŧ pőľįčy įƒ ʼnő őŧĥęř mäŧčĥįʼnģ pőľįčįęş äřę ƒőūʼnđ.",