Compare commits

...

1 Commits

Author SHA1 Message Date
amlannandy
c84749db67 chore: add notification summary input to alerts v2 flow 2025-12-17 14:16:34 +07:00
12 changed files with 239 additions and 2 deletions

View File

@@ -423,7 +423,7 @@ describe('Footer utils', () => {
description:
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
summary:
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
'The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}',
},
condition: {
alertOnAbsent: false,

View File

@@ -278,7 +278,7 @@ export function buildCreateThresholdAlertRulePayload(
labels: basicAlertState.labels,
annotations: {
description: notificationSettings.description,
summary: notificationSettings.description,
summary: notificationSettings.summary,
},
notificationSettings: notificationSettingsProps,
version: 'v5',

View File

@@ -11,6 +11,7 @@ import AdvancedOptionItem from '../EvaluationSettings/AdvancedOptionItem';
import Stepper from '../Stepper';
import MultipleNotifications from './MultipleNotifications';
import NotificationMessage from './NotificationMessage';
import NotificationSummary from './NotificationSummary';
function NotificationSettings(): JSX.Element {
const {
@@ -84,6 +85,7 @@ function NotificationSettings(): JSX.Element {
<div className="notification-settings-container">
<Stepper stepNumber={3} label="Notification settings" />
<NotificationMessage />
<NotificationSummary />
<div className="notification-settings-content">
<MultipleNotifications />
<AdvancedOptionItem

View File

@@ -0,0 +1,43 @@
import { Tooltip, Typography } from 'antd';
import TextArea from 'antd/lib/input/TextArea';
import { Info } from 'lucide-react';
import { useCreateAlertState } from '../context';
function NotificationSummary(): JSX.Element {
const {
notificationSettings,
setNotificationSettings,
} = useCreateAlertState();
return (
<div className="notification-summary-container">
<div className="notification-summary-header">
<div className="notification-summary-header-content">
<Typography.Text className="notification-summary-header-title">
Notification Summary
<Tooltip title="Customize the summary content sent in alert notifications. Template variables like {{alertname}}, {{value}}, and {{threshold}} will be replaced with actual values when the alert fires.">
<Info size={16} />
</Tooltip>
</Typography.Text>
<Typography.Text className="notification-summary-header-description">
Custom summary content for alert notifications. Use template variables to
include dynamic information.
</Typography.Text>
</div>
</div>
<TextArea
value={notificationSettings.summary}
onChange={(e): void =>
setNotificationSettings({
type: 'SET_SUMMARY',
payload: e.target.value,
})
}
placeholder="Enter notification summary..."
/>
</div>
);
}
export default NotificationSummary;

View File

@@ -0,0 +1,75 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as createAlertContext from 'container/CreateAlertV2/context';
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
import NotificationSummary from '../NotificationSummary';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
const mockSetNotificationSettings = jest.fn();
const initialNotificationSettingsState = createMockAlertContextState()
.notificationSettings;
jest.spyOn(createAlertContext, 'useCreateAlertState').mockReturnValue(
createMockAlertContextState({
notificationSettings: {
...initialNotificationSettingsState,
summary: '',
},
setNotificationSettings: mockSetNotificationSettings,
}),
);
describe('NotificationSummary', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders textarea with message and placeholder', () => {
render(<NotificationSummary />);
expect(screen.getByText('Notification Summary')).toBeInTheDocument();
const textarea = screen.getByPlaceholderText('Enter notification summary...');
expect(textarea).toBeInTheDocument();
});
it('updates notification summary when textarea value changes', async () => {
const user = userEvent.setup();
render(<NotificationSummary />);
const textarea = screen.getByPlaceholderText('Enter notification summary...');
await user.type(textarea, 'x');
expect(mockSetNotificationSettings).toHaveBeenLastCalledWith({
type: 'SET_SUMMARY',
payload: 'x',
});
});
it('displays existing description value', () => {
jest.spyOn(createAlertContext, 'useCreateAlertState').mockImplementation(
() =>
({
notificationSettings: {
summary: 'Existing summary',
},
setNotificationSettings: mockSetNotificationSettings,
} as any),
);
render(<NotificationSummary />);
const textarea = screen.getByDisplayValue('Existing summary');
expect(textarea).toBeInTheDocument();
});
});

View File

@@ -63,6 +63,66 @@
}
}
.notification-summary-container {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: -8px;
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-400);
padding: 16px;
.notification-summary-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.notification-summary-header-content {
display: flex;
flex-direction: column;
gap: 8px;
.notification-summary-header-title {
display: flex;
gap: 8px;
align-items: center;
color: var(--bg-vanilla-300);
font-family: Inter;
font-size: 14px;
font-weight: 500;
}
.notification-summary-header-description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-weight: 400;
}
}
.notification-summary-header-actions {
.ant-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
color: var(--bg-robin-400);
}
}
}
textarea {
height: 150px;
background: var(--bg-ink-400);
border: 1px solid var(--bg-slate-200);
border-radius: 4px;
color: var(--bg-vanilla-400) !important;
font-family: Inter;
font-size: 14px;
}
}
.notification-settings-content {
display: flex;
flex-direction: column;
@@ -290,6 +350,35 @@
}
}
.notification-summary-container {
background-color: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-300);
.notification-summary-header {
.notification-summary-header-content {
.notification-summary-header-title {
color: var(--bg-ink-300);
}
.notification-summary-header-description {
color: var(--bg-ink-400);
}
}
.notification-summary-header-actions {
.ant-btn {
color: var(--bg-robin-500);
}
}
}
textarea {
background: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400) !important;
}
}
.notification-settings-content {
background-color: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-300);

View File

@@ -198,6 +198,8 @@ describe('CreateAlertV2 utils', () => {
},
description:
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
summary:
'The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}',
routingPolicies: true,
});
});

View File

@@ -626,6 +626,21 @@ describe('CreateAlertV2 Context Utils', () => {
});
});
it('should set summary', () => {
const summary = 'Custom alert summary with {{$value}}';
const result = notificationSettingsReducer(
INITIAL_NOTIFICATION_SETTINGS_STATE,
{
type: 'SET_SUMMARY',
payload: summary,
},
);
expect(result).toEqual({
...INITIAL_NOTIFICATION_SETTINGS_STATE,
summary,
});
});
it(TEST_RESET_TO_INITIAL_STATE, () => {
const modifiedState: NotificationSettingsState = {
multipleNotifications: ['channel1'],
@@ -636,6 +651,7 @@ describe('CreateAlertV2 Context Utils', () => {
conditions: ['firing'],
},
description: 'Modified description',
summary: 'Modified summary',
routingPolicies: true,
};
const result = notificationSettingsReducer(modifiedState, {
@@ -654,6 +670,7 @@ describe('CreateAlertV2 Context Utils', () => {
conditions: ['nodata'],
},
description: 'New description',
summary: 'New summary',
routingPolicies: true,
};
const result = notificationSettingsReducer(

View File

@@ -179,6 +179,8 @@ export const RE_NOTIFICATION_TIME_UNIT_OPTIONS = [
export const NOTIFICATION_MESSAGE_PLACEHOLDER =
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})';
export const NOTIFICATION_SUMMARY_PLACEHOLDER =
'The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}';
export const RE_NOTIFICATION_CONDITION_OPTIONS = [
{ value: 'firing', label: 'Firing' },
@@ -194,5 +196,6 @@ export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
conditions: [],
},
description: NOTIFICATION_MESSAGE_PLACEHOLDER,
summary: NOTIFICATION_SUMMARY_PLACEHOLDER,
routingPolicies: false,
};

View File

@@ -251,6 +251,7 @@ export interface NotificationSettingsState {
conditions: ('firing' | 'nodata')[];
};
description: string;
summary: string;
routingPolicies: boolean;
}
@@ -269,6 +270,7 @@ export type NotificationSettingsAction =
};
}
| { type: 'SET_DESCRIPTION'; payload: string }
| { type: 'SET_SUMMARY'; payload: string }
| { type: 'SET_ROUTING_POLICIES'; payload: boolean }
| { type: 'SET_INITIAL_STATE'; payload: NotificationSettingsState }
| { type: 'RESET' };

View File

@@ -241,6 +241,8 @@ export const notificationSettingsReducer = (
return { ...state, reNotification: action.payload };
case 'SET_DESCRIPTION':
return { ...state, description: action.payload };
case 'SET_SUMMARY':
return { ...state, summary: action.payload };
case 'SET_ROUTING_POLICIES':
return { ...state, routingPolicies: action.payload };
case 'RESET':

View File

@@ -178,6 +178,7 @@ export function getNotificationSettingsStateFromAlertDef(
alertDef: PostableAlertRuleV2,
): NotificationSettingsState {
const description = alertDef.annotations?.description || '';
const summary = alertDef.annotations?.summary || '';
const multipleNotifications = alertDef.notificationSettings?.groupBy || [];
const routingPolicies = alertDef.notificationSettings?.usePolicy || false;
@@ -197,6 +198,7 @@ export function getNotificationSettingsStateFromAlertDef(
return {
...INITIAL_NOTIFICATION_SETTINGS_STATE,
description,
summary,
multipleNotifications,
routingPolicies,
reNotification: {