chore: add notification settings section to create alert (#9162)

This commit is contained in:
Amlan Kumar Nandy 2025-09-24 10:22:05 +07:00 committed by GitHub
parent 2c59c1196d
commit a54c3a3d7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1145 additions and 1 deletions

View File

@ -8,6 +8,7 @@ import AlertCondition from './AlertCondition';
import { CreateAlertProvider } from './context';
import CreateAlertHeader from './CreateAlertHeader';
import EvaluationSettings from './EvaluationSettings';
import NotificationSettings from './NotificationSettings';
import QuerySection from './QuerySection';
import { showCondensedLayout } from './utils';
@ -27,6 +28,7 @@ function CreateAlertV2({
<QuerySection />
<AlertCondition />
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
<NotificationSettings />
</div>
</CreateAlertProvider>
);

View File

@ -49,3 +49,40 @@
user-select: none;
}
}
.lightMode {
.time-input-container {
.time-input-field {
background-color: var(--bg-vanilla-300);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&::placeholder {
color: var(--bg-ink-300);
}
&:hover {
border-color: var(--bg-ink-300);
}
&:focus {
border-color: var(--bg-ink-300);
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
}
&:disabled {
background-color: var(--bg-vanilla-300);
color: var(--bg-ink-300);
cursor: not-allowed;
&:hover {
border-color: var(--bg-vanilla-300);
}
}
}
.time-input-separator {
color: var(--bg-ink-300);
}
}
}

View File

@ -3,6 +3,7 @@ import {
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_EVALUATION_WINDOW_STATE,
INITIAL_NOTIFICATION_SETTINGS_STATE,
} from 'container/CreateAlertV2/context/constants';
import {
EvaluationWindowState,
@ -23,6 +24,8 @@ export const createMockAlertContextState = (
setAdvancedOptions: jest.fn(),
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
setEvaluationWindow: jest.fn(),
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
setNotificationSettings: jest.fn(),
...overrides,
});

View File

@ -0,0 +1,97 @@
import { Select, Tooltip, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Info } from 'lucide-react';
import { useMemo } from 'react';
import { useCreateAlertState } from '../context';
function MultipleNotifications(): JSX.Element {
const {
notificationSettings,
setNotificationSettings,
} = useCreateAlertState();
const { currentQuery } = useQueryBuilder();
const spaceAggregationOptions = useMemo(() => {
const allGroupBys = currentQuery.builder.queryData?.reduce<string[]>(
(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 = (
<div>
<Select
options={spaceAggregationOptions}
onChange={(value): void => {
setNotificationSettings({
type: 'SET_MULTIPLE_NOTIFICATIONS',
payload: value,
});
}}
value={notificationSettings.multipleNotifications}
mode="multiple"
placeholder={placeholder}
disabled={!isMultipleNotificationsEnabled}
aria-disabled={!isMultipleNotificationsEnabled}
maxTagCount={3}
/>
{isMultipleNotificationsEnabled && (
<Typography.Paragraph className="multiple-notifications-select-description">
{notificationSettings.multipleNotifications?.length
? `Alerts with same ${notificationSettings.multipleNotifications?.join(
', ',
)} will be grouped`
: 'Empty = all matching alerts combined into one notification'}
</Typography.Paragraph>
)}
</div>
);
if (!isMultipleNotificationsEnabled) {
input = (
<Tooltip title="Add 'Group by' fields to your query to enable alert grouping">
{input}
</Tooltip>
);
}
return input;
}, [
isMultipleNotificationsEnabled,
notificationSettings.multipleNotifications,
setNotificationSettings,
spaceAggregationOptions,
]);
return (
<div className="multiple-notifications-container">
<div className="multiple-notifications-header">
<Typography.Text className="multiple-notifications-header-title">
Group alerts by{' '}
<Tooltip title="Group similar alerts together to reduce notification volume. Leave empty to combine all matching alerts into one notification without grouping.">
<Info size={16} />
</Tooltip>
</Typography.Text>
<Typography.Text className="multiple-notifications-header-description">
Combine alerts with the same field values into a single notification.
</Typography.Text>
</div>
{multipleNotificationsInput}
</div>
);
}
export default MultipleNotifications;

View File

@ -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 = (
<div className="template-variable-content">
<Typography.Text strong>Available Template Variables:</Typography.Text>
{templateVariables.map((item) => (
<div className="template-variable-content-item" key={item.variable}>
<code>{item.variable}</code>
<Typography.Text>{item.description}</Typography.Text>
</div>
))}
</div>
);
return (
<div className="notification-message-container">
<div className="notification-message-header">
<div className="notification-message-header-content">
<Typography.Text className="notification-message-header-title">
Notification Message
<Tooltip title="Customize the message 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-message-header-description">
Custom message content for alert notifications. Use template variables to
include dynamic information.
</Typography.Text>
</div>
<div className="notification-message-header-actions">
<Popover content={templateVariableContent}>
<Button type="text">
<Info size={12} />
Variables
</Button>
</Popover>
</div>
</div>
<TextArea
value={notificationSettings.description}
onChange={(e): void =>
setNotificationSettings({
type: 'SET_DESCRIPTION',
payload: e.target.value,
})
}
placeholder="Enter notification message..."
/>
</div>
);
}
export default NotificationMessage;

View File

@ -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 = (
<div className="repeat-notifications-input">
<Typography.Text>Every</Typography.Text>
<Input
value={notificationSettings.reNotification.value}
placeholder="Enter time interval..."
disabled={!notificationSettings.reNotification.enabled}
type="number"
onChange={(e): void => {
setNotificationSettings({
type: 'SET_RE_NOTIFICATION',
payload: {
enabled: notificationSettings.reNotification.enabled,
value: parseInt(e.target.value, 10),
unit: notificationSettings.reNotification.unit,
conditions: notificationSettings.reNotification.conditions,
},
});
}}
/>
<Select
value={notificationSettings.reNotification.unit || null}
placeholder="Select unit"
disabled={!notificationSettings.reNotification.enabled}
options={RE_NOTIFICATION_UNIT_OPTIONS}
onChange={(value): void => {
setNotificationSettings({
type: 'SET_RE_NOTIFICATION',
payload: {
enabled: notificationSettings.reNotification.enabled,
value: notificationSettings.reNotification.value,
unit: value,
conditions: notificationSettings.reNotification.conditions,
},
});
}}
/>
<Typography.Text>while</Typography.Text>
<Select
mode="multiple"
value={notificationSettings.reNotification.conditions || null}
placeholder="Select conditions"
disabled={!notificationSettings.reNotification.enabled}
options={RE_NOTIFICATION_CONDITION_OPTIONS}
onChange={(value): void => {
setNotificationSettings({
type: 'SET_RE_NOTIFICATION',
payload: {
enabled: notificationSettings.reNotification.enabled,
value: notificationSettings.reNotification.value,
unit: notificationSettings.reNotification.unit,
conditions: value,
},
});
}}
/>
</div>
);
return (
<div className="notification-settings-container">
<Stepper
stepNumber={showCondensedLayoutFlag ? 3 : 4}
label="Notification settings"
/>
<NotificationMessage />
<div className="notification-settings-content">
<MultipleNotifications />
<AdvancedOptionItem
title="Repeat notifications"
description="Send periodic notifications while the alert condition remains active."
tooltipText="Continue sending periodic notifications while the alert condition persists. Useful for ensuring critical alerts aren't missed during long-running incidents. Configure how often to repeat and under what conditions."
input={repeatNotificationsInput}
onToggle={(): void => {
setNotificationSettings({
type: 'SET_RE_NOTIFICATION',
payload: {
...notificationSettings.reNotification,
enabled: !notificationSettings.reNotification.enabled,
},
});
}}
/>
</div>
</div>
);
}
export default NotificationSettings;

View File

@ -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(<MultipleNotifications />);
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(<MultipleNotifications />);
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(<MultipleNotifications />);
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(<MultipleNotifications />);
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();
});
});

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 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(<NotificationMessage />);
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(<NotificationMessage />);
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(<NotificationMessage />);
const textarea = screen.getByDisplayValue('Existing message');
expect(textarea).toBeInTheDocument();
});
});

View File

@ -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 => (
<div data-testid="multiple-notifications">MultipleNotifications</div>
),
}),
);
jest.mock(
'container/CreateAlertV2/NotificationSettings/NotificationMessage',
() => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="notification-message">NotificationMessage</div>
),
}),
);
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(<NotificationSettings />);
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(<NotificationSettings />);
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(<NotificationSettings />);
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(<NotificationSettings />);
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(<NotificationSettings />);
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: [],
},
});
});
});
});

View File

@ -0,0 +1,3 @@
import NotificationSettings from './NotificationSettings';
export default NotificationSettings;

View File

@ -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);
}
}
}

View File

@ -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,
};

View File

@ -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<ICreateAlertContextProps | null>(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,
],
);

View File

@ -14,6 +14,8 @@ export interface ICreateAlertContextProps {
setAdvancedOptions: Dispatch<AdvancedOptionsAction>;
evaluationWindow: EvaluationWindowState;
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
notificationSettings: NotificationSettingsState;
setNotificationSettings: Dispatch<NotificationSettingsAction>;
}
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' };

View File

@ -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;
}
};