(
+ (acc, query) => {
+ const groupByKeys = query.groupBy?.map((groupBy) => groupBy.key) || [];
+ return [...acc, ...groupByKeys];
+ },
+ [],
+ );
+ const uniqueGroupBys = [...new Set(allGroupBys)];
+ return uniqueGroupBys.map((key) => ({
+ label: key,
+ value: key,
+ }));
+ }, [currentQuery.builder.queryData]);
+
+ const isMultipleNotificationsEnabled = spaceAggregationOptions.length > 0;
+
+ const multipleNotificationsInput = useMemo(() => {
+ const placeholder = isMultipleNotificationsEnabled
+ ? 'Select fields to group by (optional)'
+ : 'No grouping fields available';
+ let input = (
+
+
+ );
+ if (!isMultipleNotificationsEnabled) {
+ input = (
+
+ {input}
+
+ );
+ }
+ return input;
+ }, [
+ isMultipleNotificationsEnabled,
+ notificationSettings.multipleNotifications,
+ setNotificationSettings,
+ spaceAggregationOptions,
+ ]);
+
+ return (
+
+
+
+ Group alerts by{' '}
+
+
+
+
+
+ Combine alerts with the same field values into a single notification.
+
+
+ {multipleNotificationsInput}
+
+ );
+}
+
+export default MultipleNotifications;
diff --git a/frontend/src/container/CreateAlertV2/NotificationSettings/NotificationMessage.tsx b/frontend/src/container/CreateAlertV2/NotificationSettings/NotificationMessage.tsx
new file mode 100644
index 000000000000..ae1136fc28df
--- /dev/null
+++ b/frontend/src/container/CreateAlertV2/NotificationSettings/NotificationMessage.tsx
@@ -0,0 +1,92 @@
+import { Button, Popover, Tooltip, Typography } from 'antd';
+import TextArea from 'antd/lib/input/TextArea';
+import { Info } from 'lucide-react';
+
+import { useCreateAlertState } from '../context';
+
+function NotificationMessage(): JSX.Element {
+ const {
+ notificationSettings,
+ setNotificationSettings,
+ } = useCreateAlertState();
+
+ const templateVariables = [
+ { variable: '{{alertname}}', description: 'Name of the alert rule' },
+ {
+ variable: '{{value}}',
+ description: 'Current value that triggered the alert',
+ },
+ {
+ variable: '{{threshold}}',
+ description: 'Threshold value from alert condition',
+ },
+ { variable: '{{unit}}', description: 'Unit of measurement for the metric' },
+ {
+ variable: '{{severity}}',
+ description: 'Alert severity level (Critical, Warning, Info)',
+ },
+ {
+ variable: '{{queryname}}',
+ description: 'Name of the query that triggered the alert',
+ },
+ {
+ variable: '{{labels}}',
+ description: 'All labels associated with the alert',
+ },
+ {
+ variable: '{{timestamp}}',
+ description: 'Timestamp when alert was triggered',
+ },
+ ];
+
+ const templateVariableContent = (
+
+
Available Template Variables:
+ {templateVariables.map((item) => (
+
+ {item.variable}
+ {item.description}
+
+ ))}
+
+ );
+
+ return (
+
+
+
+
+ Notification Message
+
+
+
+
+
+ Custom message content for alert notifications. Use template variables to
+ include dynamic information.
+
+
+
+
+
+ );
+}
+
+export default NotificationMessage;
diff --git a/frontend/src/container/CreateAlertV2/NotificationSettings/NotificationSettings.tsx b/frontend/src/container/CreateAlertV2/NotificationSettings/NotificationSettings.tsx
new file mode 100644
index 000000000000..c099b40a2dbe
--- /dev/null
+++ b/frontend/src/container/CreateAlertV2/NotificationSettings/NotificationSettings.tsx
@@ -0,0 +1,112 @@
+import './styles.scss';
+
+import { Input, Select, Typography } from 'antd';
+
+import { useCreateAlertState } from '../context';
+import {
+ ADVANCED_OPTIONS_TIME_UNIT_OPTIONS as RE_NOTIFICATION_UNIT_OPTIONS,
+ RE_NOTIFICATION_CONDITION_OPTIONS,
+} from '../context/constants';
+import AdvancedOptionItem from '../EvaluationSettings/AdvancedOptionItem';
+import Stepper from '../Stepper';
+import { showCondensedLayout } from '../utils';
+import MultipleNotifications from './MultipleNotifications';
+import NotificationMessage from './NotificationMessage';
+
+function NotificationSettings(): JSX.Element {
+ const showCondensedLayoutFlag = showCondensedLayout();
+
+ const {
+ notificationSettings,
+ setNotificationSettings,
+ } = useCreateAlertState();
+
+ const repeatNotificationsInput = (
+
+ Every
+ {
+ setNotificationSettings({
+ type: 'SET_RE_NOTIFICATION',
+ payload: {
+ enabled: notificationSettings.reNotification.enabled,
+ value: parseInt(e.target.value, 10),
+ unit: notificationSettings.reNotification.unit,
+ conditions: notificationSettings.reNotification.conditions,
+ },
+ });
+ }}
+ />
+
+ );
+
+ return (
+
+
+
+
+
+
{
+ setNotificationSettings({
+ type: 'SET_RE_NOTIFICATION',
+ payload: {
+ ...notificationSettings.reNotification,
+ enabled: !notificationSettings.reNotification.enabled,
+ },
+ });
+ }}
+ />
+
+
+ );
+}
+
+export default NotificationSettings;
diff --git a/frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/MultipleNotifications.test.tsx b/frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/MultipleNotifications.test.tsx
new file mode 100644
index 000000000000..b0442d1c0ab9
--- /dev/null
+++ b/frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/MultipleNotifications.test.tsx
@@ -0,0 +1,172 @@
+/* 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 {
+ INITIAL_ALERT_THRESHOLD_STATE,
+ INITIAL_NOTIFICATION_SETTINGS_STATE,
+} from 'container/CreateAlertV2/context/constants';
+import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
+
+import MultipleNotifications from '../MultipleNotifications';
+
+jest.mock('uplot', () => {
+ const paths = {
+ spline: jest.fn(),
+ bars: jest.fn(),
+ };
+ const uplotMock = jest.fn(() => ({
+ paths,
+ }));
+ return {
+ paths,
+ default: uplotMock,
+ };
+});
+jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
+ useQueryBuilder: jest.fn(),
+}));
+
+const TEST_QUERY = 'test-query';
+const TEST_GROUP_BY_FIELDS = [{ key: 'service' }, { key: 'environment' }];
+const TRUE = 'true';
+const FALSE = 'false';
+const COMBOBOX_ROLE = 'combobox';
+const ARIA_DISABLED_ATTR = 'aria-disabled';
+const mockSetNotificationSettings = jest.fn();
+const mockUseQueryBuilder = {
+ currentQuery: {
+ builder: {
+ queryData: [
+ {
+ queryName: TEST_QUERY,
+ groupBy: [],
+ },
+ ],
+ },
+ },
+};
+
+const initialAlertThresholdState = createMockAlertContextState().thresholdState;
+jest.spyOn(createAlertContext, 'useCreateAlertState').mockReturnValue(
+ createMockAlertContextState({
+ thresholdState: {
+ ...initialAlertThresholdState,
+ selectedQuery: TEST_QUERY,
+ },
+ setNotificationSettings: mockSetNotificationSettings,
+ }),
+);
+
+describe('MultipleNotifications', () => {
+ const { useQueryBuilder } = jest.requireMock(
+ 'hooks/queryBuilder/useQueryBuilder',
+ );
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ useQueryBuilder.mockReturnValue(mockUseQueryBuilder);
+ });
+
+ it('should render the multiple notifications component with no grouping fields and disabled input by default', () => {
+ render();
+ expect(screen.getByText('Group alerts by')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'Combine alerts with the same field values into a single notification.',
+ ),
+ ).toBeInTheDocument();
+ expect(screen.getByText('No grouping fields available')).toBeInTheDocument();
+ const select = screen.getByRole(COMBOBOX_ROLE);
+ expect(select).toHaveAttribute(ARIA_DISABLED_ATTR, TRUE);
+ });
+
+ it('should render the multiple notifications component with grouping fields and enabled input when space aggregation options are set', () => {
+ useQueryBuilder.mockReturnValue({
+ currentQuery: {
+ builder: {
+ queryData: [
+ {
+ queryName: TEST_QUERY,
+ groupBy: TEST_GROUP_BY_FIELDS,
+ },
+ ],
+ },
+ },
+ });
+ render();
+
+ expect(
+ screen.getByText(
+ 'Empty = all matching alerts combined into one notification',
+ ),
+ ).toBeInTheDocument();
+ const select = screen.getByRole(COMBOBOX_ROLE);
+ expect(select).toHaveAttribute(ARIA_DISABLED_ATTR, FALSE);
+ });
+
+ it('should render the multiple notifications component with grouping fields and enabled input when space aggregation options are set and multiple notifications are enabled', () => {
+ useQueryBuilder.mockReturnValue({
+ currentQuery: {
+ builder: {
+ queryData: [
+ {
+ queryName: TEST_QUERY,
+ groupBy: TEST_GROUP_BY_FIELDS,
+ },
+ ],
+ },
+ },
+ });
+ jest.spyOn(createAlertContext, 'useCreateAlertState').mockReturnValue(
+ createMockAlertContextState({
+ thresholdState: {
+ ...INITIAL_ALERT_THRESHOLD_STATE,
+ selectedQuery: TEST_QUERY,
+ },
+ notificationSettings: {
+ ...INITIAL_NOTIFICATION_SETTINGS_STATE,
+ multipleNotifications: ['service', 'environment'],
+ },
+ setNotificationSettings: mockSetNotificationSettings,
+ }),
+ );
+
+ render();
+
+ expect(
+ screen.getByText('Alerts with same service, environment will be grouped'),
+ ).toBeInTheDocument();
+ const select = screen.getByRole(COMBOBOX_ROLE);
+ expect(select).toHaveAttribute(ARIA_DISABLED_ATTR, FALSE);
+ });
+
+ it('should render unique group by options from all queries', async () => {
+ useQueryBuilder.mockReturnValue({
+ currentQuery: {
+ builder: {
+ queryData: [
+ {
+ queryName: 'test-query-1',
+ groupBy: [{ key: 'http.status_code' }],
+ },
+ {
+ queryName: 'test-query-2',
+ groupBy: [{ key: 'service' }],
+ },
+ ],
+ },
+ },
+ });
+
+ render();
+
+ const select = screen.getByRole(COMBOBOX_ROLE);
+ await userEvent.click(select);
+
+ expect(
+ screen.getByRole('option', { name: 'http.status_code' }),
+ ).toBeInTheDocument();
+ expect(screen.getByRole('option', { name: 'service' })).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/NotificationMessage.test.tsx b/frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/NotificationMessage.test.tsx
new file mode 100644
index 000000000000..7793e5bc540e
--- /dev/null
+++ b/frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/NotificationMessage.test.tsx
@@ -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 NotificationMessage from '../NotificationMessage';
+
+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,
+ description: '',
+ },
+ setNotificationSettings: mockSetNotificationSettings,
+ }),
+);
+
+describe('NotificationMessage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders textarea with message and placeholder', () => {
+ render();
+ expect(screen.getByText('Notification Message')).toBeInTheDocument();
+ const textarea = screen.getByPlaceholderText('Enter notification message...');
+ expect(textarea).toBeInTheDocument();
+ });
+
+ it('updates notification settings when textarea value changes', async () => {
+ const user = userEvent.setup();
+ render();
+ const textarea = screen.getByPlaceholderText('Enter notification message...');
+ await user.type(textarea, 'x');
+ expect(mockSetNotificationSettings).toHaveBeenLastCalledWith({
+ type: 'SET_DESCRIPTION',
+ payload: 'x',
+ });
+ });
+
+ it('displays existing description value', () => {
+ jest.spyOn(createAlertContext, 'useCreateAlertState').mockImplementation(
+ () =>
+ ({
+ notificationSettings: {
+ description: 'Existing message',
+ },
+ setNotificationSettings: mockSetNotificationSettings,
+ } as any),
+ );
+
+ render();
+
+ const textarea = screen.getByDisplayValue('Existing message');
+ expect(textarea).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/NotificationSettings.test.tsx b/frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/NotificationSettings.test.tsx
new file mode 100644
index 000000000000..e2b74c113ec9
--- /dev/null
+++ b/frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/NotificationSettings.test.tsx
@@ -0,0 +1,120 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import * as createAlertContext from 'container/CreateAlertV2/context';
+import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
+import * as utils from 'container/CreateAlertV2/utils';
+
+import NotificationSettings from '../NotificationSettings';
+
+jest.mock(
+ 'container/CreateAlertV2/NotificationSettings/MultipleNotifications',
+ () => ({
+ __esModule: true,
+ default: (): JSX.Element => (
+ MultipleNotifications
+ ),
+ }),
+);
+jest.mock(
+ 'container/CreateAlertV2/NotificationSettings/NotificationMessage',
+ () => ({
+ __esModule: true,
+ default: (): JSX.Element => (
+ NotificationMessage
+ ),
+ }),
+);
+
+const initialNotificationSettings = createMockAlertContextState()
+ .notificationSettings;
+const mockSetNotificationSettings = jest.fn();
+jest.spyOn(createAlertContext, 'useCreateAlertState').mockReturnValue(
+ createMockAlertContextState({
+ setNotificationSettings: mockSetNotificationSettings,
+ }),
+);
+
+const REPEAT_NOTIFICATIONS_TEXT = 'Repeat notifications';
+const ENTER_TIME_INTERVAL_TEXT = 'Enter time interval...';
+
+describe('NotificationSettings', () => {
+ it('renders the notification settings tab with step number 4 and default values', () => {
+ render();
+ expect(screen.getByText('Notification settings')).toBeInTheDocument();
+ expect(screen.getByText('4')).toBeInTheDocument();
+ expect(screen.getByTestId('multiple-notifications')).toBeInTheDocument();
+ expect(screen.getByTestId('notification-message')).toBeInTheDocument();
+ expect(screen.getByText(REPEAT_NOTIFICATIONS_TEXT)).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'Send periodic notifications while the alert condition remains active.',
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it('renders the notification settings tab with step number 3 in condensed layout', () => {
+ jest.spyOn(utils, 'showCondensedLayout').mockReturnValueOnce(true);
+ render();
+ expect(screen.getByText('Notification settings')).toBeInTheDocument();
+ expect(screen.getByText('3')).toBeInTheDocument();
+ expect(screen.getByTestId('multiple-notifications')).toBeInTheDocument();
+ expect(screen.getByTestId('notification-message')).toBeInTheDocument();
+ });
+
+ describe('Repeat notifications', () => {
+ it('renders the repeat notifications with inputs hidden when the repeat notifications switch is off', () => {
+ render();
+ expect(screen.getByText(REPEAT_NOTIFICATIONS_TEXT)).toBeInTheDocument();
+ expect(screen.getByText('Every')).not.toBeVisible();
+ expect(
+ screen.getByPlaceholderText(ENTER_TIME_INTERVAL_TEXT),
+ ).not.toBeVisible();
+ });
+
+ it('toggles the repeat notifications switch and shows the inputs', () => {
+ render();
+ expect(screen.getByText(REPEAT_NOTIFICATIONS_TEXT)).toBeInTheDocument();
+
+ expect(screen.getByText('Every')).not.toBeVisible();
+ expect(
+ screen.getByPlaceholderText(ENTER_TIME_INTERVAL_TEXT),
+ ).not.toBeVisible();
+
+ fireEvent.click(screen.getByRole('switch'));
+
+ expect(screen.getByText('Every')).toBeVisible();
+ expect(screen.getByPlaceholderText(ENTER_TIME_INTERVAL_TEXT)).toBeVisible();
+ });
+
+ it('updates state when the repeat notifications input is changed', () => {
+ jest.spyOn(createAlertContext, 'useCreateAlertState').mockReturnValue(
+ createMockAlertContextState({
+ setNotificationSettings: mockSetNotificationSettings,
+ notificationSettings: {
+ ...initialNotificationSettings,
+ reNotification: {
+ ...initialNotificationSettings.reNotification,
+ enabled: true,
+ },
+ },
+ }),
+ );
+
+ render();
+ expect(screen.getByText(REPEAT_NOTIFICATIONS_TEXT)).toBeInTheDocument();
+
+ fireEvent.change(screen.getByPlaceholderText(ENTER_TIME_INTERVAL_TEXT), {
+ target: { value: '13' },
+ });
+
+ expect(mockSetNotificationSettings).toHaveBeenLastCalledWith({
+ type: 'SET_RE_NOTIFICATION',
+ payload: {
+ enabled: true,
+ value: 13,
+ unit: 'min',
+ conditions: [],
+ },
+ });
+ });
+ });
+});
diff --git a/frontend/src/container/CreateAlertV2/NotificationSettings/index.ts b/frontend/src/container/CreateAlertV2/NotificationSettings/index.ts
new file mode 100644
index 000000000000..5a383626f054
--- /dev/null
+++ b/frontend/src/container/CreateAlertV2/NotificationSettings/index.ts
@@ -0,0 +1,3 @@
+import NotificationSettings from './NotificationSettings';
+
+export default NotificationSettings;
diff --git a/frontend/src/container/CreateAlertV2/NotificationSettings/styles.scss b/frontend/src/container/CreateAlertV2/NotificationSettings/styles.scss
new file mode 100644
index 000000000000..4e1348678f9a
--- /dev/null
+++ b/frontend/src/container/CreateAlertV2/NotificationSettings/styles.scss
@@ -0,0 +1,346 @@
+.notification-settings-container {
+ display: flex;
+ flex-direction: column;
+ margin: 0 16px;
+
+ .notification-message-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-message-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 16px;
+
+ .notification-message-header-content {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ .notification-message-header-title {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ color: var(--bg-vanilla-300);
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .notification-message-header-description {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 400;
+ }
+ }
+
+ .notification-message-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;
+ background-color: var(--bg-ink-400);
+ border: 1px solid var(--bg-slate-400);
+ padding: 16px;
+ margin-top: 16px;
+
+ .repeat-notifications-input {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .ant-input {
+ width: 120px;
+ border: 1px solid var(--bg-slate-100);
+ }
+
+ .ant-select {
+ .ant-select-selector {
+ width: 120px;
+ }
+ }
+
+ .ant-select-multiple {
+ .ant-select-selector {
+ width: 200px;
+ }
+ }
+ }
+
+ .multiple-notifications-container {
+ display: flex;
+ padding: 4px 16px 16px 16px;
+ border-bottom: 1px solid var(--bg-slate-400);
+ justify-content: space-between;
+
+ .multiple-notifications-header {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ .ant-typography {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ }
+
+ .multiple-notifications-header-title {
+ color: var(--bg-vanilla-300);
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .multiple-notifications-header-description {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 400;
+ }
+ }
+
+ .ant-select {
+ width: 300px;
+ }
+
+ .multiple-notifications-select-description {
+ font-size: 10px;
+ color: var(--bg-vanilla-400);
+ margin-top: 4px;
+ }
+ }
+
+ .re-notification-container {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ background-color: var(--bg-ink-400);
+ border: 1px solid var(--bg-slate-400);
+ padding: 16px;
+ margin-top: 16px;
+
+ .advanced-option-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 16px;
+
+ .advanced-option-item-left-content {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+
+ .advanced-option-item-title {
+ color: var(--bg-vanilla-300);
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .advanced-option-item-description {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 400;
+ }
+ }
+ }
+
+ .border-bottom {
+ border-bottom: 1px solid var(--bg-slate-400);
+ width: 100%;
+ margin-left: -16px;
+ margin-right: -32px;
+ }
+
+ .re-notification-condition {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: nowrap;
+
+ .ant-typography {
+ font-size: 14px;
+ font-weight: 400;
+ color: var(--bg-vanilla-400);
+ white-space: nowrap;
+ }
+
+ .ant-select {
+ width: 200px;
+ height: 32px;
+ flex-shrink: 0;
+ .ant-select-selector {
+ border: 1px solid var(--bg-slate-400);
+ }
+ }
+
+ .ant-input {
+ width: 200px;
+ flex-shrink: 0;
+ border: 1px solid var(--bg-slate-400);
+ }
+ }
+ }
+ }
+}
+
+.template-variable-content {
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+
+ .template-variable-content-item {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+
+ code {
+ background-color: var(--bg-slate-500);
+ color: var(--bg-vanilla-400);
+ padding: 2px 4px;
+ }
+ }
+}
+
+.lightMode {
+ .notification-settings-container {
+ .notification-message-container {
+ background-color: var(--bg-vanilla-200);
+ border: 1px solid var(--bg-vanilla-300);
+
+ .notification-message-header {
+ .notification-message-header-content {
+ .notification-message-header-title {
+ color: var(--bg-ink-300);
+ }
+
+ .notification-message-header-description {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .notification-message-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);
+
+ .repeat-notifications-input {
+ .ant-input {
+ border: 1px solid var(--bg-vanilla-300);
+ }
+ }
+
+ .multiple-notifications-container {
+ background-color: var(--bg-vanilla-200);
+ border: 1px solid var(--bg-vanilla-300);
+
+ .multiple-notifications-header {
+ .multiple-notifications-header-title {
+ color: var(--bg-ink-300);
+ }
+
+ .multiple-notifications-header-description {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .multiple-notifications-select-description {
+ color: var(--bg-ink-400);
+ }
+
+ .border-bottom {
+ border-bottom: 1px solid var(--bg-vanilla-300);
+ }
+ }
+ }
+
+ .re-notification-container {
+ background-color: var(--bg-vanilla-200);
+ border: 1px solid var(--bg-vanilla-300);
+
+ .advanced-option-item {
+ .advanced-option-item-left-content {
+ .advanced-option-item-title {
+ color: var(--bg-ink-300);
+ }
+
+ .advanced-option-item-description {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+
+ .border-bottom {
+ border-bottom: 1px solid var(--bg-vanilla-300);
+ }
+
+ .re-notification-condition {
+ .ant-typography {
+ color: var(--bg-ink-400);
+ }
+
+ .ant-select {
+ .ant-select-selector {
+ border: 1px solid var(--bg-vanilla-300);
+ }
+ }
+
+ .ant-input {
+ border: 1px solid var(--bg-vanilla-300);
+ }
+ }
+ }
+ }
+
+ .template-variable-content-item {
+ code {
+ background-color: var(--bg-vanilla-300);
+ color: var(--bg-ink-400);
+ }
+ }
+}
diff --git a/frontend/src/container/CreateAlertV2/context/constants.ts b/frontend/src/container/CreateAlertV2/context/constants.ts
index 5d37c39217ab..cf640c8904c8 100644
--- a/frontend/src/container/CreateAlertV2/context/constants.ts
+++ b/frontend/src/container/CreateAlertV2/context/constants.ts
@@ -13,6 +13,7 @@ import {
AlertThresholdState,
Algorithm,
EvaluationWindowState,
+ NotificationSettingsState,
Seasonality,
Threshold,
TimeDuration,
@@ -170,3 +171,22 @@ export const ADVANCED_OPTIONS_TIME_UNIT_OPTIONS = [
{ value: UniversalYAxisUnit.HOURS, label: 'Hours' },
{ value: UniversalYAxisUnit.DAYS, label: 'Days' },
];
+
+export const NOTIFICATION_MESSAGE_PLACEHOLDER =
+ 'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})';
+
+export const RE_NOTIFICATION_CONDITION_OPTIONS = [
+ { value: 'firing', label: 'Firing' },
+ { value: 'no-data', label: 'No Data' },
+];
+
+export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
+ multipleNotifications: [],
+ reNotification: {
+ enabled: false,
+ value: 1,
+ unit: UniversalYAxisUnit.MINUTES,
+ conditions: [],
+ },
+ description: NOTIFICATION_MESSAGE_PLACEHOLDER,
+};
diff --git a/frontend/src/container/CreateAlertV2/context/index.tsx b/frontend/src/container/CreateAlertV2/context/index.tsx
index 14221ba25130..e3d2ec73576f 100644
--- a/frontend/src/container/CreateAlertV2/context/index.tsx
+++ b/frontend/src/container/CreateAlertV2/context/index.tsx
@@ -18,6 +18,7 @@ import {
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_EVALUATION_WINDOW_STATE,
+ INITIAL_NOTIFICATION_SETTINGS_STATE,
} from './constants';
import { ICreateAlertContextProps, ICreateAlertProviderProps } from './types';
import {
@@ -27,6 +28,7 @@ import {
buildInitialAlertDef,
evaluationWindowReducer,
getInitialAlertTypeFromURL,
+ notificationSettingsReducer,
} from './utils';
const CreateAlertContext = createContext(null);
@@ -94,6 +96,11 @@ export function CreateAlertProvider(
INITIAL_ADVANCED_OPTIONS_STATE,
);
+ const [notificationSettings, setNotificationSettings] = useReducer(
+ notificationSettingsReducer,
+ INITIAL_NOTIFICATION_SETTINGS_STATE,
+ );
+
useEffect(() => {
setThresholdState({
type: 'RESET',
@@ -112,6 +119,8 @@ export function CreateAlertProvider(
setEvaluationWindow,
advancedOptions,
setAdvancedOptions,
+ notificationSettings,
+ setNotificationSettings,
}),
[
alertState,
@@ -120,6 +129,7 @@ export function CreateAlertProvider(
thresholdState,
evaluationWindow,
advancedOptions,
+ notificationSettings,
],
);
diff --git a/frontend/src/container/CreateAlertV2/context/types.ts b/frontend/src/container/CreateAlertV2/context/types.ts
index a76909054cc4..85d4c7284d8a 100644
--- a/frontend/src/container/CreateAlertV2/context/types.ts
+++ b/frontend/src/container/CreateAlertV2/context/types.ts
@@ -14,6 +14,8 @@ export interface ICreateAlertContextProps {
setAdvancedOptions: Dispatch;
evaluationWindow: EvaluationWindowState;
setEvaluationWindow: Dispatch;
+ notificationSettings: NotificationSettingsState;
+ setNotificationSettings: Dispatch;
}
export interface ICreateAlertProviderProps {
@@ -38,7 +40,8 @@ export type CreateAlertAction =
| { type: 'SET_ALERT_NAME'; payload: string }
| { type: 'SET_ALERT_DESCRIPTION'; payload: string }
| { type: 'SET_ALERT_LABELS'; payload: Labels }
- | { type: 'SET_Y_AXIS_UNIT'; payload: string | undefined };
+ | { type: 'SET_Y_AXIS_UNIT'; payload: string | undefined }
+ | { type: 'RESET' };
export interface Threshold {
id: string;
@@ -190,3 +193,31 @@ export type EvaluationWindowAction =
| { type: 'RESET' };
export type EvaluationCadenceMode = 'default' | 'custom' | 'rrule';
+
+export interface NotificationSettingsState {
+ multipleNotifications: string[] | null;
+ reNotification: {
+ enabled: boolean;
+ value: number;
+ unit: string;
+ conditions: ('firing' | 'no-data')[];
+ };
+ description: string;
+}
+
+export type NotificationSettingsAction =
+ | {
+ type: 'SET_MULTIPLE_NOTIFICATIONS';
+ payload: string[] | null;
+ }
+ | {
+ type: 'SET_RE_NOTIFICATION';
+ payload: {
+ enabled: boolean;
+ value: number;
+ unit: string;
+ conditions: ('firing' | 'no-data')[];
+ };
+ }
+ | { type: 'SET_DESCRIPTION'; payload: string }
+ | { type: 'RESET' };
diff --git a/frontend/src/container/CreateAlertV2/context/utils.tsx b/frontend/src/container/CreateAlertV2/context/utils.tsx
index aa295262c012..3b8c973e0e50 100644
--- a/frontend/src/container/CreateAlertV2/context/utils.tsx
+++ b/frontend/src/container/CreateAlertV2/context/utils.tsx
@@ -13,8 +13,10 @@ import { DataSource } from 'types/common/queryBuilder';
import {
INITIAL_ADVANCED_OPTIONS_STATE,
+ INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_EVALUATION_WINDOW_STATE,
+ INITIAL_NOTIFICATION_SETTINGS_STATE,
} from './constants';
import {
AdvancedOptionsAction,
@@ -25,6 +27,8 @@ import {
CreateAlertAction,
EvaluationWindowAction,
EvaluationWindowState,
+ NotificationSettingsAction,
+ NotificationSettingsState,
} from './types';
export const alertCreationReducer = (
@@ -52,6 +56,8 @@ export const alertCreationReducer = (
...state,
yAxisUnit: action.payload,
};
+ case 'RESET':
+ return INITIAL_ALERT_STATE;
default:
return state;
}
@@ -172,3 +178,21 @@ export const evaluationWindowReducer = (
return state;
}
};
+
+export const notificationSettingsReducer = (
+ state: NotificationSettingsState,
+ action: NotificationSettingsAction,
+): NotificationSettingsState => {
+ switch (action.type) {
+ case 'SET_MULTIPLE_NOTIFICATIONS':
+ return { ...state, multipleNotifications: action.payload };
+ case 'SET_RE_NOTIFICATION':
+ return { ...state, reNotification: action.payload };
+ case 'SET_DESCRIPTION':
+ return { ...state, description: action.payload };
+ case 'RESET':
+ return INITIAL_NOTIFICATION_SETTINGS_STATE;
+ default:
+ return state;
+ }
+};