mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-18 16:07:10 +00:00
chore: add notification settings section to create alert (#9162)
This commit is contained in:
parent
2c59c1196d
commit
a54c3a3d7f
@ -8,6 +8,7 @@ import AlertCondition from './AlertCondition';
|
|||||||
import { CreateAlertProvider } from './context';
|
import { CreateAlertProvider } from './context';
|
||||||
import CreateAlertHeader from './CreateAlertHeader';
|
import CreateAlertHeader from './CreateAlertHeader';
|
||||||
import EvaluationSettings from './EvaluationSettings';
|
import EvaluationSettings from './EvaluationSettings';
|
||||||
|
import NotificationSettings from './NotificationSettings';
|
||||||
import QuerySection from './QuerySection';
|
import QuerySection from './QuerySection';
|
||||||
import { showCondensedLayout } from './utils';
|
import { showCondensedLayout } from './utils';
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ function CreateAlertV2({
|
|||||||
<QuerySection />
|
<QuerySection />
|
||||||
<AlertCondition />
|
<AlertCondition />
|
||||||
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
|
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
|
||||||
|
<NotificationSettings />
|
||||||
</div>
|
</div>
|
||||||
</CreateAlertProvider>
|
</CreateAlertProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -49,3 +49,40 @@
|
|||||||
user-select: none;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
INITIAL_ALERT_STATE,
|
INITIAL_ALERT_STATE,
|
||||||
INITIAL_ALERT_THRESHOLD_STATE,
|
INITIAL_ALERT_THRESHOLD_STATE,
|
||||||
INITIAL_EVALUATION_WINDOW_STATE,
|
INITIAL_EVALUATION_WINDOW_STATE,
|
||||||
|
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||||
} from 'container/CreateAlertV2/context/constants';
|
} from 'container/CreateAlertV2/context/constants';
|
||||||
import {
|
import {
|
||||||
EvaluationWindowState,
|
EvaluationWindowState,
|
||||||
@ -23,6 +24,8 @@ export const createMockAlertContextState = (
|
|||||||
setAdvancedOptions: jest.fn(),
|
setAdvancedOptions: jest.fn(),
|
||||||
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
|
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
|
||||||
setEvaluationWindow: jest.fn(),
|
setEvaluationWindow: jest.fn(),
|
||||||
|
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||||
|
setNotificationSettings: jest.fn(),
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import NotificationSettings from './NotificationSettings';
|
||||||
|
|
||||||
|
export default NotificationSettings;
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
AlertThresholdState,
|
AlertThresholdState,
|
||||||
Algorithm,
|
Algorithm,
|
||||||
EvaluationWindowState,
|
EvaluationWindowState,
|
||||||
|
NotificationSettingsState,
|
||||||
Seasonality,
|
Seasonality,
|
||||||
Threshold,
|
Threshold,
|
||||||
TimeDuration,
|
TimeDuration,
|
||||||
@ -170,3 +171,22 @@ export const ADVANCED_OPTIONS_TIME_UNIT_OPTIONS = [
|
|||||||
{ value: UniversalYAxisUnit.HOURS, label: 'Hours' },
|
{ value: UniversalYAxisUnit.HOURS, label: 'Hours' },
|
||||||
{ value: UniversalYAxisUnit.DAYS, label: 'Days' },
|
{ 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,
|
||||||
|
};
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
INITIAL_ALERT_STATE,
|
INITIAL_ALERT_STATE,
|
||||||
INITIAL_ALERT_THRESHOLD_STATE,
|
INITIAL_ALERT_THRESHOLD_STATE,
|
||||||
INITIAL_EVALUATION_WINDOW_STATE,
|
INITIAL_EVALUATION_WINDOW_STATE,
|
||||||
|
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { ICreateAlertContextProps, ICreateAlertProviderProps } from './types';
|
import { ICreateAlertContextProps, ICreateAlertProviderProps } from './types';
|
||||||
import {
|
import {
|
||||||
@ -27,6 +28,7 @@ import {
|
|||||||
buildInitialAlertDef,
|
buildInitialAlertDef,
|
||||||
evaluationWindowReducer,
|
evaluationWindowReducer,
|
||||||
getInitialAlertTypeFromURL,
|
getInitialAlertTypeFromURL,
|
||||||
|
notificationSettingsReducer,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
const CreateAlertContext = createContext<ICreateAlertContextProps | null>(null);
|
const CreateAlertContext = createContext<ICreateAlertContextProps | null>(null);
|
||||||
@ -94,6 +96,11 @@ export function CreateAlertProvider(
|
|||||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [notificationSettings, setNotificationSettings] = useReducer(
|
||||||
|
notificationSettingsReducer,
|
||||||
|
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setThresholdState({
|
setThresholdState({
|
||||||
type: 'RESET',
|
type: 'RESET',
|
||||||
@ -112,6 +119,8 @@ export function CreateAlertProvider(
|
|||||||
setEvaluationWindow,
|
setEvaluationWindow,
|
||||||
advancedOptions,
|
advancedOptions,
|
||||||
setAdvancedOptions,
|
setAdvancedOptions,
|
||||||
|
notificationSettings,
|
||||||
|
setNotificationSettings,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
alertState,
|
alertState,
|
||||||
@ -120,6 +129,7 @@ export function CreateAlertProvider(
|
|||||||
thresholdState,
|
thresholdState,
|
||||||
evaluationWindow,
|
evaluationWindow,
|
||||||
advancedOptions,
|
advancedOptions,
|
||||||
|
notificationSettings,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,8 @@ export interface ICreateAlertContextProps {
|
|||||||
setAdvancedOptions: Dispatch<AdvancedOptionsAction>;
|
setAdvancedOptions: Dispatch<AdvancedOptionsAction>;
|
||||||
evaluationWindow: EvaluationWindowState;
|
evaluationWindow: EvaluationWindowState;
|
||||||
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
||||||
|
notificationSettings: NotificationSettingsState;
|
||||||
|
setNotificationSettings: Dispatch<NotificationSettingsAction>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateAlertProviderProps {
|
export interface ICreateAlertProviderProps {
|
||||||
@ -38,7 +40,8 @@ export type CreateAlertAction =
|
|||||||
| { type: 'SET_ALERT_NAME'; payload: string }
|
| { type: 'SET_ALERT_NAME'; payload: string }
|
||||||
| { type: 'SET_ALERT_DESCRIPTION'; payload: string }
|
| { type: 'SET_ALERT_DESCRIPTION'; payload: string }
|
||||||
| { type: 'SET_ALERT_LABELS'; payload: Labels }
|
| { 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 {
|
export interface Threshold {
|
||||||
id: string;
|
id: string;
|
||||||
@ -190,3 +193,31 @@ export type EvaluationWindowAction =
|
|||||||
| { type: 'RESET' };
|
| { type: 'RESET' };
|
||||||
|
|
||||||
export type EvaluationCadenceMode = 'default' | 'custom' | 'rrule';
|
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' };
|
||||||
|
|||||||
@ -13,8 +13,10 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||||
|
INITIAL_ALERT_STATE,
|
||||||
INITIAL_ALERT_THRESHOLD_STATE,
|
INITIAL_ALERT_THRESHOLD_STATE,
|
||||||
INITIAL_EVALUATION_WINDOW_STATE,
|
INITIAL_EVALUATION_WINDOW_STATE,
|
||||||
|
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import {
|
import {
|
||||||
AdvancedOptionsAction,
|
AdvancedOptionsAction,
|
||||||
@ -25,6 +27,8 @@ import {
|
|||||||
CreateAlertAction,
|
CreateAlertAction,
|
||||||
EvaluationWindowAction,
|
EvaluationWindowAction,
|
||||||
EvaluationWindowState,
|
EvaluationWindowState,
|
||||||
|
NotificationSettingsAction,
|
||||||
|
NotificationSettingsState,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const alertCreationReducer = (
|
export const alertCreationReducer = (
|
||||||
@ -52,6 +56,8 @@ export const alertCreationReducer = (
|
|||||||
...state,
|
...state,
|
||||||
yAxisUnit: action.payload,
|
yAxisUnit: action.payload,
|
||||||
};
|
};
|
||||||
|
case 'RESET':
|
||||||
|
return INITIAL_ALERT_STATE;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -172,3 +178,21 @@ export const evaluationWindowReducer = (
|
|||||||
return state;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user