Compare commits

...

7 Commits

Author SHA1 Message Date
srikanthccv
3540fc7ae2 chore: some edits 2025-09-25 21:27:49 +05:30
Srikanth Chekuri
61efbd248c Merge branch 'main' into ux-changes 2025-09-25 19:47:07 +05:30
Srikanth Chekuri
35192eecd8 Merge branch 'main' into ux-changes 2025-09-24 23:35:37 +05:30
amlannandy
7b14490266 chore: fix ci 2025-09-24 17:01:18 +07:00
amlannandy
a3ee84af48 chore: ux and light mode changes 2025-09-24 10:44:48 +07:00
amlannandy
db79d3a0de chore: fix ci 2025-09-24 10:44:48 +07:00
amlannandy
0a466ee3e9 feat: add notifcation settings section to create alert 2025-09-24 10:44:47 +07:00
33 changed files with 1168 additions and 206 deletions

View File

@@ -1,6 +1,7 @@
import './styles.scss';
import '../EvaluationSettings/styles.scss';
import { Button, Select, Typography } from 'antd';
import { Button, Select, Tooltip, Typography } from 'antd';
import getAllChannels from 'api/channels/getAll';
import classNames from 'classnames';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -25,6 +26,7 @@ import { UpdateThreshold } from './types';
import {
getCategoryByOptionId,
getCategorySelectOptionByName,
getMatchTypeTooltip,
getQueryNames,
} from './utils';
@@ -85,6 +87,35 @@ function AlertThreshold(): JSX.Element {
});
};
const matchTypeOptionsWithTooltips = THRESHOLD_MATCH_TYPE_OPTIONS.map(
(option) => ({
...option,
label: (
<Tooltip
title={getMatchTypeTooltip(option.value, thresholdState.operator)}
placement="left"
overlayClassName="copyable-tooltip"
overlayStyle={{
maxWidth: '450px',
minWidth: '400px',
}}
overlayInnerStyle={{
padding: '12px 16px',
userSelect: 'text',
WebkitUserSelect: 'text',
MozUserSelect: 'text',
msUserSelect: 'text',
}}
mouseEnterDelay={0.2}
trigger={['hover', 'click']}
destroyTooltipOnHide={false}
>
<span style={{ display: 'block', width: '100%' }}>{option.label}</span>
</Tooltip>
),
}),
);
const evaluationWindowContext = showCondensedLayoutFlag ? (
<EvaluationSettings />
) : (
@@ -114,8 +145,7 @@ function AlertThreshold(): JSX.Element {
style={{ width: 80 }}
options={queryNames}
/>
</div>
<div className="alert-condition-sentence">
<Typography.Text className="sentence-text">is</Typography.Text>
<Select
value={thresholdState.operator}
onChange={(value): void => {
@@ -124,7 +154,7 @@ function AlertThreshold(): JSX.Element {
payload: value,
});
}}
style={{ width: 120 }}
style={{ width: 180 }}
options={THRESHOLD_OPERATOR_OPTIONS}
/>
<Typography.Text className="sentence-text">
@@ -138,8 +168,8 @@ function AlertThreshold(): JSX.Element {
payload: value,
});
}}
style={{ width: 140 }}
options={THRESHOLD_MATCH_TYPE_OPTIONS}
style={{ width: 180 }}
options={matchTypeOptionsWithTooltips}
/>
<Typography.Text className="sentence-text">
during the {evaluationWindowContext}

View File

@@ -1,7 +1,9 @@
import { Button, Input, Select, Space, Tooltip, Typography } from 'antd';
import { ChartLine, CircleX } from 'lucide-react';
import { Button, Input, Select, Tooltip, Typography } from 'antd';
import { ChartLine, CircleX, Trash } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useCreateAlertState } from '../context';
import { AlertThresholdOperator } from '../context/types';
import { ThresholdItemProps } from './types';
function ThresholdItem({
@@ -12,6 +14,7 @@ function ThresholdItem({
channels,
units,
}: ThresholdItemProps): JSX.Element {
const { thresholdState } = useCreateAlertState();
const [showRecoveryThreshold, setShowRecoveryThreshold] = useState(false);
const yAxisUnitSelect = useMemo(() => {
@@ -45,6 +48,32 @@ function ThresholdItem({
return component;
}, [units, threshold.unit, updateThreshold, threshold.id]);
const getOperatorSymbol = (): string => {
switch (thresholdState.operator) {
case AlertThresholdOperator.IS_ABOVE:
return '>';
case AlertThresholdOperator.IS_BELOW:
return '<';
case AlertThresholdOperator.IS_EQUAL_TO:
return '=';
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
return '!=';
default:
return '';
}
};
const addRecoveryThreshold = (): void => {
// Recovery threshold - hidden for now
// setShowRecoveryThreshold(true);
// updateThreshold(threshold.id, 'recoveryThresholdValue', 0);
};
const removeRecoveryThreshold = (): void => {
setShowRecoveryThreshold(false);
updateThreshold(threshold.id, 'recoveryThresholdValue', null);
};
return (
<div key={threshold.id} className="threshold-item">
<div className="threshold-row">
@@ -54,80 +83,99 @@ function ThresholdItem({
style={{ backgroundColor: threshold.color }}
/>
</div>
<Space className="threshold-controls">
<div className="threshold-inputs">
<Input.Group>
<Input
placeholder="Enter threshold name"
value={threshold.label}
onChange={(e): void =>
updateThreshold(threshold.id, 'label', e.target.value)
}
style={{ width: 260 }}
/>
<Input
placeholder="Enter threshold value"
value={threshold.thresholdValue}
onChange={(e): void =>
updateThreshold(threshold.id, 'thresholdValue', e.target.value)
}
style={{ width: 210 }}
/>
{yAxisUnitSelect}
</Input.Group>
</div>
<Typography.Text className="sentence-text">to</Typography.Text>
<div className="threshold-controls">
<Input
placeholder="Enter threshold name"
value={threshold.label}
onChange={(e): void =>
updateThreshold(threshold.id, 'label', e.target.value)
}
style={{ width: 200 }}
/>
<Typography.Text className="sentence-text">on value</Typography.Text>
<Typography.Text className="sentence-text highlighted-text">
{getOperatorSymbol()}
</Typography.Text>
<Input
placeholder="Enter threshold value"
value={threshold.thresholdValue}
onChange={(e): void =>
updateThreshold(threshold.id, 'thresholdValue', e.target.value)
}
style={{ width: 100 }}
type="number"
/>
{yAxisUnitSelect}
<Typography.Text className="sentence-text">send to</Typography.Text>
<Select
value={threshold.channels}
onChange={(value): void =>
updateThreshold(threshold.id, 'channels', value)
}
style={{ width: 260 }}
style={{ width: 350 }}
options={channels.map((channel) => ({
value: channel.id,
label: channel.name,
}))}
mode="multiple"
placeholder="Select notification channels"
showSearch
maxTagCount={2}
maxTagPlaceholder={(omittedValues): string =>
`+${omittedValues.length} more`
}
maxTagTextLength={10}
filterOption={(input, option): boolean =>
option?.label?.toLowerCase().includes(input.toLowerCase()) || false
}
/>
{/* Recovery threshold - hidden for now */}
{/* {showRecoveryThreshold && (
<>
<Typography.Text className="sentence-text">recover on</Typography.Text>
<Input
placeholder="Enter recovery threshold value"
value={threshold.recoveryThresholdValue ?? ''}
onChange={(e): void =>
updateThreshold(threshold.id, 'recoveryThresholdValue', e.target.value)
}
style={{ width: 100 }}
type="number"
/>
<Tooltip title="Remove recovery threshold">
<Button
type="default"
icon={<Trash size={16} />}
onClick={removeRecoveryThreshold}
className="icon-btn"
/>
</Tooltip>
</>
)} */}
<Button.Group>
{!showRecoveryThreshold && (
<Button
type="default"
icon={<ChartLine size={16} />}
className="icon-btn"
onClick={(): void => setShowRecoveryThreshold(true)}
/>
)}
{/* {!showRecoveryThreshold && (
<Tooltip title="Add recovery threshold">
<Button
type="default"
icon={<ChartLine size={16} />}
className="icon-btn"
onClick={addRecoveryThreshold}
/>
</Tooltip>
)} */}
{showRemoveButton && (
<Button
type="default"
icon={<CircleX size={16} />}
onClick={(): void => removeThreshold(threshold.id)}
className="icon-btn"
/>
<Tooltip title="Remove threshold">
<Button
type="default"
icon={<CircleX size={16} />}
onClick={(): void => removeThreshold(threshold.id)}
className="icon-btn"
/>
</Tooltip>
)}
</Button.Group>
</Space>
</div>
</div>
{showRecoveryThreshold && (
<Input.Group className="recovery-threshold-input-group">
<Input
placeholder="Recovery threshold"
disabled
style={{ width: 260 }}
className="recovery-threshold-label"
/>
<Input
placeholder="Enter recovery threshold value"
value={threshold.recoveryThresholdValue}
onChange={(e): void =>
updateThreshold(threshold.id, 'recoveryThresholdValue', e.target.value)
}
style={{ width: 210 }}
/>
</Input.Group>
)}
</div>
);
}

View File

@@ -99,7 +99,7 @@ jest.mock('container/NewWidget/RightContainer/alertFomatCategories', () => ({
const TEST_STRINGS = {
ADD_THRESHOLD: 'Add Threshold',
AT_LEAST_ONCE: 'AT LEAST ONCE',
IS_ABOVE: 'IS ABOVE',
IS_ABOVE: 'ABOVE',
} as const;
const createTestQueryClient = (): QueryClient =>
@@ -125,7 +125,10 @@ const renderAlertThreshold = (): ReturnType<typeof render> => {
};
const verifySelectRenders = (title: string): void => {
const select = screen.getByTitle(title);
let select = screen.queryByTitle(title);
if (!select) {
select = screen.getByText(title);
}
expect(select).toBeInTheDocument();
};

View File

@@ -2,15 +2,36 @@
/* eslint-disable react/jsx-props-no-spreading */
import { fireEvent, render, screen } from '@testing-library/react';
import { DefaultOptionType } from 'antd/es/select';
import {
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
} from 'container/CreateAlertV2/context/constants';
import { Channels } from 'types/api/channels/getAll';
import * as context from '../../context';
import ThresholdItem from '../ThresholdItem';
import { ThresholdItemProps } from '../types';
// Mock the enableRecoveryThreshold utility
jest.mock('../../utils', () => ({
enableRecoveryThreshold: jest.fn(() => true),
}));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock: any = jest.fn(() => ({
paths,
}));
uplotMock.paths = paths;
return uplotMock;
});
const mockSetAlertState = jest.fn();
const mockSetThresholdState = jest.fn();
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
alertState: INITIAL_ALERT_STATE,
setAlertState: mockSetAlertState,
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
setThresholdState: mockSetThresholdState,
} as any);
const TEST_CONSTANTS = {
THRESHOLD_ID: 'test-threshold-1',
@@ -21,6 +42,7 @@ const TEST_CONSTANTS = {
CHANNEL_2: 'channel-2',
CHANNEL_3: 'channel-3',
EMAIL_CHANNEL_NAME: 'Email Channel',
EMAIL_CHANNEL_TRUNCATED: 'Email Chan...',
ENTER_THRESHOLD_NAME: 'Enter threshold name',
ENTER_THRESHOLD_VALUE: 'Enter threshold value',
ENTER_RECOVERY_THRESHOLD_VALUE: 'Enter recovery threshold value',
@@ -122,7 +144,7 @@ describe('ThresholdItem', () => {
const valueInput = screen.getByPlaceholderText(
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
);
expect(valueInput).toHaveValue('100');
expect(valueInput).toHaveValue(100);
});
it('renders unit selector with correct value', () => {
@@ -135,9 +157,8 @@ describe('ThresholdItem', () => {
it('renders channels selector with correct value', () => {
renderThresholdItem();
// Check for the channels selector by looking for the displayed text
expect(
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_TRUNCATED),
).toBeInTheDocument();
});
@@ -251,7 +272,9 @@ describe('ThresholdItem', () => {
const recoveryButton = buttons[0]; // First button is the recovery button
fireEvent.click(recoveryButton);
expect(screen.getByPlaceholderText('Recovery threshold')).toBeInTheDocument();
expect(
screen.getByPlaceholderText('Enter recovery threshold value'),
).toBeInTheDocument();
expect(
screen.getByPlaceholderText(TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE),
).toBeInTheDocument();
@@ -295,7 +318,7 @@ describe('ThresholdItem', () => {
// Check that channels are rendered as multiple select
expect(
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_TRUNCATED),
).toBeInTheDocument();
// Should be able to select multiple channels
@@ -318,7 +341,7 @@ describe('ThresholdItem', () => {
renderThresholdItem({ threshold: emptyThreshold });
expect(screen.getByPlaceholderText('Enter threshold name')).toHaveValue('');
expect(screen.getByPlaceholderText('Enter threshold value')).toHaveValue('0');
expect(screen.getByPlaceholderText('Enter threshold value')).toHaveValue(0);
});
it('renders with correct input widths', () => {
@@ -331,13 +354,13 @@ describe('ThresholdItem', () => {
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
);
expect(labelInput).toHaveStyle('width: 260px');
expect(valueInput).toHaveStyle('width: 210px');
expect(labelInput).toHaveStyle('width: 200px');
expect(valueInput).toHaveStyle('width: 100px');
});
it('renders channels selector with correct width', () => {
renderThresholdItem();
verifySelectorWidth(1, '260px');
verifySelectorWidth(1, '350px');
});
it('renders unit selector with correct width', () => {
@@ -357,30 +380,7 @@ describe('ThresholdItem', () => {
const recoveryValueInput = screen.getByPlaceholderText(
TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE,
);
expect(recoveryValueInput).toHaveValue('80');
});
it('renders recovery threshold label as disabled', () => {
renderThresholdItem();
showRecoveryThreshold();
const recoveryLabelInput = screen.getByPlaceholderText('Recovery threshold');
expect(recoveryLabelInput).toBeDisabled();
});
it('renders correct channel options', () => {
renderThresholdItem();
// Check that channels are rendered
expect(
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
).toBeInTheDocument();
// Should be able to select different channels
const channelSelectors = screen.getAllByRole('combobox');
const channelSelector = channelSelectors[1]; // Second combobox is the channels selector
fireEvent.change(channelSelector, { target: { value: 'channel-2' } });
expect(screen.getByText('Slack Channel')).toBeInTheDocument();
expect(recoveryValueInput).toHaveValue(80);
});
it('handles threshold without channels', () => {

View File

@@ -67,7 +67,7 @@
padding-right: 72px;
background-color: var(--bg-ink-500);
border: 1px solid var(--bg-slate-400);
width: fit-content;
width: 100%;
.alert-condition-sentences {
display: flex;
@@ -90,7 +90,7 @@
}
.ant-select {
width: 240px !important;
width: 240px;
.ant-select-selector {
background-color: var(--bg-ink-300);
@@ -148,6 +148,7 @@
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
.ant-input {
background-color: var(--bg-ink-400);
@@ -293,7 +294,8 @@
.ant-btn {
display: flex;
align-items: center;
width: 240px;
min-width: 240px;
width: auto;
justify-content: space-between;
background-color: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
@@ -301,6 +303,7 @@
.evaluate-alert-conditions-button-left {
color: var(--bg-vanilla-400);
font-size: 12px;
flex-shrink: 0;
}
.evaluate-alert-conditions-button-right {
@@ -308,6 +311,7 @@
align-items: center;
color: var(--bg-vanilla-400);
gap: 8px;
flex-shrink: 0;
.evaluate-alert-conditions-button-right-text {
font-size: 12px;
@@ -318,3 +322,229 @@
}
}
}
.lightMode {
.alert-condition-container {
.alert-condition {
.alert-condition-tabs {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
.explorer-view-option {
border-left: 0.5px solid var(--bg-vanilla-300);
border-bottom: 0.5px solid var(--bg-vanilla-300);
&.active-tab {
background-color: var(--bg-vanilla-100);
&:hover {
background-color: var(--bg-vanilla-100) !important;
}
}
&:disabled {
background-color: var(--bg-vanilla-300);
}
&:hover {
color: var(--bg-ink-400);
}
}
}
}
}
.alert-threshold-container,
.anomaly-threshold-container {
background-color: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
.alert-condition-sentences {
.alert-condition-sentence {
.sentence-text {
color: var(--text-ink-400);
}
.ant-select {
.ant-select-selector {
background-color: var(--bg-vanilla-300);
border: 1px solid var(--bg-vanilla-300);
color: var(--text-ink-400);
&: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);
}
}
.ant-select-selection-item {
color: var(--bg-ink-400);
}
.ant-select-arrow {
color: var(--bg-ink-400);
}
}
}
}
.thresholds-section {
.threshold-item {
.threshold-row {
.threshold-controls {
.threshold-inputs {
display: flex;
align-items: center;
gap: 8px;
}
.ant-input {
background-color: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&: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);
}
}
.ant-select {
.ant-select-selector {
background-color: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&: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);
}
}
.ant-select-selection-item {
color: var(--bg-ink-400);
}
.ant-select-arrow {
color: var(--bg-ink-400);
}
}
.icon-btn {
color: var(--bg-ink-400);
border: 1px solid var(--bg-vanilla-300);
}
}
}
.recovery-threshold-input-group {
.recovery-threshold-btn {
color: var(--bg-ink-400);
background-color: var(--bg-vanilla-200) !important;
border: 1px solid var(--bg-vanilla-300);
}
.ant-input {
background-color: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&: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);
}
}
}
.add-threshold-btn {
border: 1px dashed var(--bg-vanilla-300);
color: var(--bg-ink-300);
&:hover {
border-color: var(--bg-ink-300);
color: var(--bg-ink-400);
}
}
}
}
}
.condensed-evaluation-settings-container {
.ant-btn {
background-color: var(--bg-vanilla-300);
border: 1px solid var(--bg-vanilla-300);
min-width: 240px;
width: auto;
.evaluate-alert-conditions-button-left {
color: var(--bg-ink-400);
flex-shrink: 0;
}
.evaluate-alert-conditions-button-right {
color: var(--bg-ink-400);
flex-shrink: 0;
.evaluate-alert-conditions-button-right-text {
background-color: var(--bg-vanilla-300);
}
}
}
}
}
.highlighted-text {
font-weight: bold;
color: var(--bg-robin-400);
margin: 0 4px;
}
// Tooltip styles
.tooltip-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
.tooltip-description {
margin-bottom: 8px;
span {
font-weight: bold;
color: var(--bg-robin-400);
}
}
.tooltip-example {
margin-bottom: 8px;
color: #8b92a0;
}
.tooltip-link {
.tooltip-link-text {
color: #1890ff;
font-size: 11px;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}

View File

@@ -8,7 +8,7 @@ export type UpdateThreshold = {
(
thresholdId: string,
field: Exclude<keyof Threshold, 'channels'>,
value: string,
value: string | number | null,
): void;
};

View File

@@ -1,6 +1,10 @@
import { BaseOptionType, DefaultOptionType, SelectProps } from 'antd/es/select';
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants';
import {
AlertThresholdMatchType,
AlertThresholdOperator,
} from 'container/CreateAlertV2/context/types';
import { getSelectedQueryOptions } from 'container/FormAlertRules/utils';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -44,3 +48,303 @@ export function getCategorySelectOptionByName(
) || []
);
}
const getOperatorWord = (op: AlertThresholdOperator): string => {
switch (op) {
case AlertThresholdOperator.IS_ABOVE:
return 'exceed';
case AlertThresholdOperator.IS_BELOW:
return 'fall below';
case AlertThresholdOperator.IS_EQUAL_TO:
return 'equal';
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
return 'not equal';
default:
return 'exceed';
}
};
const getThresholdValue = (op: AlertThresholdOperator): number => {
switch (op) {
case AlertThresholdOperator.IS_ABOVE:
return 80;
case AlertThresholdOperator.IS_BELOW:
return 50;
case AlertThresholdOperator.IS_EQUAL_TO:
return 100;
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
return 0;
default:
return 80;
}
};
const getDataPoints = (
matchType: AlertThresholdMatchType,
op: AlertThresholdOperator,
): number[] => {
const dataPointMap: Record<
AlertThresholdMatchType,
Record<AlertThresholdOperator, number[]>
> = {
[AlertThresholdMatchType.AT_LEAST_ONCE]: {
[AlertThresholdOperator.IS_BELOW]: [60, 45, 40, 55, 35],
[AlertThresholdOperator.IS_EQUAL_TO]: [95, 100, 105, 90, 100],
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 0, 10, 15, 0],
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
},
[AlertThresholdMatchType.ALL_THE_TIME]: {
[AlertThresholdOperator.IS_BELOW]: [45, 40, 35, 42, 38],
[AlertThresholdOperator.IS_EQUAL_TO]: [100, 100, 100, 100, 100],
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 10, 15, 8, 12],
[AlertThresholdOperator.IS_ABOVE]: [85, 87, 90, 88, 95],
[AlertThresholdOperator.ABOVE_BELOW]: [85, 87, 90, 88, 95],
},
[AlertThresholdMatchType.ON_AVERAGE]: {
[AlertThresholdOperator.IS_BELOW]: [60, 40, 45, 35, 45],
[AlertThresholdOperator.IS_EQUAL_TO]: [95, 105, 100, 95, 105],
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 10, 15, 8, 12],
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
},
[AlertThresholdMatchType.IN_TOTAL]: {
[AlertThresholdOperator.IS_BELOW]: [8, 5, 10, 12, 8],
[AlertThresholdOperator.IS_EQUAL_TO]: [20, 20, 20, 20, 20],
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [10, 15, 25, 5, 30],
[AlertThresholdOperator.IS_ABOVE]: [10, 15, 25, 5, 30],
[AlertThresholdOperator.ABOVE_BELOW]: [10, 15, 25, 5, 30],
},
[AlertThresholdMatchType.LAST]: {
[AlertThresholdOperator.IS_BELOW]: [75, 85, 90, 78, 45],
[AlertThresholdOperator.IS_EQUAL_TO]: [75, 85, 90, 78, 100],
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [75, 85, 90, 78, 25],
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
},
};
return dataPointMap[matchType]?.[op] || [75, 85, 90, 78, 95];
};
const getTooltipOperatorSymbol = (op: AlertThresholdOperator): string => {
const symbolMap: Record<AlertThresholdOperator, string> = {
[AlertThresholdOperator.IS_ABOVE]: '>',
[AlertThresholdOperator.IS_BELOW]: '<',
[AlertThresholdOperator.IS_EQUAL_TO]: '=',
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: '!=',
[AlertThresholdOperator.ABOVE_BELOW]: '>',
};
return symbolMap[op] || '>';
};
const handleTooltipClick = (
e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
): void => {
e.stopPropagation();
};
function TooltipContent({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
return (
<div
role="button"
tabIndex={0}
onClick={handleTooltipClick}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleTooltipClick(e);
}
}}
className="tooltip-content"
>
{children}
</div>
);
}
function TooltipExample({
children,
dataPoints,
operatorSymbol,
thresholdValue,
matchType,
}: {
children: React.ReactNode;
dataPoints: number[];
operatorSymbol: string;
thresholdValue: number;
matchType: AlertThresholdMatchType;
}): JSX.Element {
return (
<div className="tooltip-example">
<strong>Example:</strong>
<br />
Say, For a 5-minute window (configured in Evaluation settings), 1 min
aggregation interval (set up in query) 5{' '}
{matchType === AlertThresholdMatchType.IN_TOTAL
? 'error counts'
: 'data points'}
: [{dataPoints.join(', ')}]<br />
With threshold {operatorSymbol} {thresholdValue}: {children}
</div>
);
}
function TooltipLink(): JSX.Element {
return (
<div className="tooltip-link">
<a
href="https://signoz.io/docs"
target="_blank"
rel="noopener noreferrer"
className="tooltip-link-text"
>
Learn more
</a>
</div>
);
}
export const getMatchTypeTooltip = (
matchType: AlertThresholdMatchType,
operator: AlertThresholdOperator,
): React.ReactNode => {
const operatorSymbol = getTooltipOperatorSymbol(operator);
const operatorWord = getOperatorWord(operator);
const thresholdValue = getThresholdValue(operator);
const dataPoints = getDataPoints(matchType, operator);
const getMatchingPointsCount = (): number =>
dataPoints.filter((p) => {
switch (operator) {
case AlertThresholdOperator.IS_ABOVE:
return p > thresholdValue;
case AlertThresholdOperator.IS_BELOW:
return p < thresholdValue;
case AlertThresholdOperator.IS_EQUAL_TO:
return p === thresholdValue;
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
return p !== thresholdValue;
default:
return p > thresholdValue;
}
}).length;
switch (matchType) {
case AlertThresholdMatchType.AT_LEAST_ONCE:
return (
<TooltipContent>
<div className="tooltip-description">
Data is aggregated at each interval within your evaluation window,
creating multiple data points. This option triggers if <span>ANY</span> of
those aggregated data points crosses the threshold.
</div>
<TooltipExample
dataPoints={dataPoints}
operatorSymbol={operatorSymbol}
thresholdValue={thresholdValue}
matchType={matchType}
>
Alert triggers ({getMatchingPointsCount()} points {operatorWord}{' '}
{thresholdValue})
</TooltipExample>
<TooltipLink />
</TooltipContent>
);
case AlertThresholdMatchType.ALL_THE_TIME:
return (
<TooltipContent>
<div className="tooltip-description">
Data is aggregated at each interval within your evaluation window,
creating multiple data points. This option triggers if <span>ALL</span>{' '}
aggregated data points cross the threshold.
</div>
<TooltipExample
dataPoints={dataPoints}
operatorSymbol={operatorSymbol}
thresholdValue={thresholdValue}
matchType={matchType}
>
Alert triggers (all points {operatorWord} {thresholdValue})<br />
If any point was {thresholdValue}, no alert would fire
</TooltipExample>
<TooltipLink />
</TooltipContent>
);
case AlertThresholdMatchType.ON_AVERAGE: {
const average = (
dataPoints.reduce((a, b) => a + b, 0) / dataPoints.length
).toFixed(1);
return (
<TooltipContent>
<div className="tooltip-description">
Data is aggregated at each interval within your evaluation window,
creating multiple data points. This option triggers if the{' '}
<span>AVERAGE</span> of all aggregated data points crosses the threshold.
</div>
<TooltipExample
dataPoints={dataPoints}
operatorSymbol={operatorSymbol}
thresholdValue={thresholdValue}
matchType={matchType}
>
Alert triggers (average = {average})
</TooltipExample>
<TooltipLink />
</TooltipContent>
);
}
case AlertThresholdMatchType.IN_TOTAL: {
const total = dataPoints.reduce((a, b) => a + b, 0);
return (
<TooltipContent>
<div className="tooltip-description">
Data is aggregated at each interval within your evaluation window,
creating multiple data points. This option triggers if the{' '}
<span>SUM</span> of all aggregated data points crosses the threshold.
</div>
<TooltipExample
dataPoints={dataPoints}
operatorSymbol={operatorSymbol}
thresholdValue={thresholdValue}
matchType={matchType}
>
Alert triggers (total = {total})
</TooltipExample>
<TooltipLink />
</TooltipContent>
);
}
case AlertThresholdMatchType.LAST: {
const lastPoint = dataPoints[dataPoints.length - 1];
return (
<TooltipContent>
<div className="tooltip-description">
Data is aggregated at each interval within your evaluation window,
creating multiple data points. This option triggers based on the{' '}
<span>MOST RECENT</span> aggregated data point only.
</div>
<TooltipExample
dataPoints={dataPoints}
operatorSymbol={operatorSymbol}
thresholdValue={thresholdValue}
matchType={matchType}
>
Alert triggers (last point = {lastPoint})
</TooltipExample>
<TooltipLink />
</TooltipContent>
);
}
default:
return '';
}
};

View File

@@ -49,15 +49,6 @@ function CreateAlertHeader(): JSX.Element {
className="alert-header__input title"
placeholder="Enter alert rule name"
/>
<input
type="text"
value={alertState.description}
onChange={(e): void =>
setAlertState({ type: 'SET_ALERT_DESCRIPTION', payload: e.target.value })
}
className="alert-header__input description"
placeholder="Click to add description..."
/>
<LabelsInput
labels={alertState.labels}
onLabelsChange={(labels: Labels): void =>

View File

@@ -44,14 +44,6 @@ describe('CreateAlertHeader', () => {
expect(nameInput).toBeInTheDocument();
});
it('renders description input with placeholder', () => {
renderCreateAlertHeader();
const descriptionInput = screen.getByPlaceholderText(
'Click to add description...',
);
expect(descriptionInput).toBeInTheDocument();
});
it('renders LabelsInput component', () => {
renderCreateAlertHeader();
expect(screen.getByText('+ Add labels')).toBeInTheDocument();
@@ -65,13 +57,4 @@ describe('CreateAlertHeader', () => {
expect(nameInput).toHaveValue('Test Alert');
});
it('updates description when typing in description input', () => {
renderCreateAlertHeader();
const descriptionInput = screen.getByPlaceholderText(
'Click to add description...',
);
fireEvent.change(descriptionInput, { target: { value: 'Test Description' } });
expect(descriptionInput).toHaveValue('Test Description');
});
});

View File

@@ -149,3 +149,75 @@
}
}
}
.lightMode {
.alert-header {
background-color: var(--bg-vanilla-100);
color: var(--text-ink-100);
&__tab-bar {
background: repeating-linear-gradient(
-45deg,
#f5f5f5,
#f5f5f5 10px,
#e5e5e5 10px,
#e5e5e5 20px
);
}
&__tab {
background-color: var(--bg-vanilla-100);
color: var(--text-ink-100);
}
&__tab::before {
color: var(--bg-ink-100);
}
&__content {
background: var(--bg-vanilla-100);
}
&__input.title {
color: var(--text-ink-100);
}
&__input.description {
color: var(--text-ink-300);
}
}
.labels-input {
&__add-button {
color: var(--bg-ink-400);
border: 1px solid var(--bg-vanilla-300);
&:hover {
border-color: var(--bg-ink-300);
color: var(--bg-ink-500);
}
}
&__label-pill {
background-color: #ad7f581a;
color: var(--bg-sienna-400);
border: 1px solid var(--bg-sienna-500);
}
&__remove-button {
color: var(--bg-sienna-400);
&:hover {
color: var(--text-ink-100);
}
}
&__input {
color: var(--bg-ink-500);
&::placeholder {
color: var(--bg-ink-300);
}
}
}
}

View File

@@ -1,8 +1,12 @@
$top-nav-background-1: #0f0f0f;
$top-nav-background-2: #101010;
$top-nav-background-1-light: #f5f5f5;
$top-nav-background-2-light: #e5e5e5;
.create-alert-v2-container {
background-color: var(--bg-ink-500);
padding-bottom: 100px;
}
.top-nav-container {
@@ -15,3 +19,19 @@ $top-nav-background-2: #101010;
);
margin-bottom: 0;
}
.lightMode {
.create-alert-v2-container {
background-color: var(--bg-vanilla-100);
}
.top-nav-container {
background: repeating-linear-gradient(
-45deg,
$top-nav-background-1-light,
$top-nav-background-1-light 10px,
$top-nav-background-2-light 10px,
$top-nav-background-2-light 20px
);
}
}

View File

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

View File

@@ -81,7 +81,7 @@ function AdvancedOptions(): JSX.Element {
</div>
}
/>
<AdvancedOptionItem
{/* <AdvancedOptionItem
title="Account for data delay"
description="Shift the evaluation window backwards to account for data processing delays."
tooltipText="Use when your data takes time to arrive on the platform. For example, if logs typically arrive 5 minutes late, set a 5-minute delay so the alert checks the correct time window."
@@ -119,7 +119,7 @@ function AdvancedOptions(): JSX.Element {
/>
</div>
}
/>
/> */}
</Collapse.Panel>
</Collapse>
</div>

View File

@@ -98,13 +98,14 @@ function EvaluationCadence(): JSX.Element {
}
/>
</Input.Group>
<Button
{/* Add custom schedule - hidden for now */}
{/* <Button
className="advanced-option-item-button"
onClick={showCustomSchedule}
>
<Plus size={12} />
<Typography.Text>Add custom schedule</Typography.Text>
</Button>
</Button> */}
</div>
)}
</div>

View File

@@ -3,8 +3,8 @@ import { useMemo } from 'react';
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';
import {
CUMULATIVE_WINDOW_DESCRIPTION,
ROLLING_WINDOW_DESCRIPTION,
getCumulativeWindowDescription,
getRollingWindowDescription,
TIMEZONE_DATA,
} from '../constants';
import TimeInput from '../TimeInput';
@@ -116,7 +116,9 @@ function EvaluationWindowDetails({
if (isCurrentHour) {
return (
<div className="evaluation-window-details">
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
<Typography.Text>
{getCumulativeWindowDescription('currentHour')}
</Typography.Text>
<Typography.Text>{displayText}</Typography.Text>
<div className="select-group">
<Typography.Text>STARTING AT MINUTE</Typography.Text>
@@ -134,7 +136,9 @@ function EvaluationWindowDetails({
if (isCurrentDay) {
return (
<div className="evaluation-window-details">
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
<Typography.Text>
{getCumulativeWindowDescription('currentDay')}
</Typography.Text>
<Typography.Text>{displayText}</Typography.Text>
<div className="select-group time-select-group">
<Typography.Text>STARTING AT</Typography.Text>
@@ -159,7 +163,9 @@ function EvaluationWindowDetails({
if (isCurrentMonth) {
return (
<div className="evaluation-window-details">
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
<Typography.Text>
{getCumulativeWindowDescription('currentMonth')}
</Typography.Text>
<Typography.Text>{displayText}</Typography.Text>
<div className="select-group">
<Typography.Text>STARTING ON DAY</Typography.Text>
@@ -192,7 +198,11 @@ function EvaluationWindowDetails({
return (
<div className="evaluation-window-details">
<Typography.Text>{ROLLING_WINDOW_DESCRIPTION}</Typography.Text>
<Typography.Text>
{getRollingWindowDescription(
`${evaluationWindow.startingAt.number}${evaluationWindow.startingAt.unit}`,
)}
</Typography.Text>
<Typography.Text>Specify custom duration</Typography.Text>
<Typography.Text>{displayText}</Typography.Text>
<div className="select-group">

View File

@@ -3,10 +3,10 @@ import classNames from 'classnames';
import { Check } from 'lucide-react';
import {
CUMULATIVE_WINDOW_DESCRIPTION,
getCumulativeWindowDescription,
getRollingWindowDescription,
EVALUATION_WINDOW_TIMEFRAME,
EVALUATION_WINDOW_TYPE,
ROLLING_WINDOW_DESCRIPTION,
} from '../constants';
import {
CumulativeWindowTimeframes,
@@ -96,7 +96,9 @@ function EvaluationWindowPopover({
}
return (
<div className="selection-content">
<Typography.Text>{ROLLING_WINDOW_DESCRIPTION}</Typography.Text>
<Typography.Text>
{getRollingWindowDescription(evaluationWindow.timeframe)}
</Typography.Text>
<Button type="link">Read the docs</Button>
</div>
);
@@ -108,7 +110,9 @@ function EvaluationWindowPopover({
) {
return (
<div className="selection-content">
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
<Typography.Text>
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
</Typography.Text>
<Button type="link">Read the docs</Button>
</div>
);

View File

@@ -26,6 +26,7 @@ export const createMockAlertContextState = (
setEvaluationWindow: jest.fn(),
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
setNotificationSettings: jest.fn(),
discardAlertRule: jest.fn(),
...overrides,
});

View File

@@ -62,8 +62,76 @@ export const TIMEZONE_DATA = generateTimezoneData().map((timezone) => ({
value: timezone.value,
}));
export const CUMULATIVE_WINDOW_DESCRIPTION =
'A Cumulative Window has a fixed starting point and expands over time.';
export const getCumulativeWindowDescription = (
timeframe?: string,
): string => {
let example = '';
switch (timeframe) {
case 'currentHour':
example =
'An hourly cumulative window for error count alerts when errors exceed 100. Starting at the top of the hour, it tracks: 20 errors by :15, 55 by :30, 105 by :45 (alert fires).';
break;
case 'currentDay':
example =
'A daily cumulative window for sales alerts when total revenue exceeds $10,000. Starting at midnight, it tracks: $2,000 by 9 AM, $5,500 by noon, $11,000 by 3 PM (alert fires).';
break;
case 'currentMonth':
example =
'A monthly cumulative window for expense alerts when spending exceeds $50,000. Starting on the 1st, it tracks: $15,000 by the 7th, $32,000 by the 15th, $51,000 by the 22nd (alert fires).';
break;
default:
example =
'A daily cumulative window for sales alerts when total revenue exceeds $10,000. Starting at midnight, it tracks: $2,000 by 9 AM, $5,500 by noon, $11,000 by 3 PM (alert fires).';
}
return `Monitors data accumulated since a fixed starting point. The window grows over time, keeping all historical data from the start.\n\nExample: ${example}`;
};
export const ROLLING_WINDOW_DESCRIPTION =
'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.';
export const getRollingWindowDescription = (duration?: string): string => {
let timeWindow = '5-minute';
let examples = '14:01:00-14:06:00, 14:02:00-14:07:00';
if (duration) {
const match = duration.match(/^(\d+)([mhs])/);
if (match) {
const value = parseInt(match[1]);
const unit = match[2];
if (unit === 'm' && !isNaN(value)) {
timeWindow = `${value}-minute`;
const endMinutes1 = 1 + value;
const endMinutes2 = 2 + value;
examples = `14:01:00-14:${String(endMinutes1).padStart(2, '0')}:00, 14:02:00-14:${String(endMinutes2).padStart(2, '0')}:00`;
} else if (unit === 'h' && !isNaN(value)) {
timeWindow = `${value}-hour`;
const endHour1 = 14 + value;
const endHour2 = 14 + value;
examples = `14:00:00-${String(endHour1).padStart(2, '0')}:00:00, 14:01:00-${String(endHour2).padStart(2, '0')}:01:00`;
} else if (unit === 's' && !isNaN(value)) {
timeWindow = `${value}-second`;
examples = `14:01:00-14:01:${String(value).padStart(2, '0')}, 14:01:01-14:01:${String(1 + value).padStart(2, '0')}`;
}
} else if (duration === 'custom' || !duration) {
timeWindow = '5-minute';
examples = '14:01:00-14:06:00, 14:02:00-14:07:00';
} else {
if (duration.includes('h')) {
const hours = parseInt(duration);
if (!isNaN(hours)) {
timeWindow = `${hours}-hour`;
const endHour = 14 + hours;
examples = `14:00:00-${String(endHour).padStart(2, '0')}:00:00, 14:01:00-${String(endHour).padStart(2, '0')}:01:00`;
}
} else if (duration.includes('m')) {
const minutes = parseInt(duration);
if (!isNaN(minutes)) {
timeWindow = `${minutes}-minute`;
const endMinutes1 = 1 + minutes;
const endMinutes2 = 2 + minutes;
examples = `14:01:00-14:${String(endMinutes1).padStart(2, '0')}:00, 14:02:00-14:${String(endMinutes2).padStart(2, '0')}:00`;
}
}
}
}
return `Monitors data over a fixed time period that moves forward continuously.\n\nExample: A ${timeWindow} rolling window for error rate alerts with 1 minute evaluation cadence. Unlike fixed windows, this checks continuously: ${examples}, etc.`;
};

View File

@@ -0,0 +1,40 @@
import './styles.scss';
import { Button, Typography } from 'antd';
import { Check, Send, X } from 'lucide-react';
import { useCreateAlertState } from '../context';
function Footer(): JSX.Element {
const { discardAlertRule } = useCreateAlertState();
const handleDiscard = (): void => discardAlertRule();
const handleTestNotification = (): void => {
// TODO: Implement test notification
};
const handleSaveAlert = (): void => {
// TODO: Implement save alert
};
return (
<div className="create-alert-v2-footer">
<Button type="text" onClick={handleDiscard}>
<X size={14} /> Discard
</Button>
<div className="button-group">
<Button type="default" onClick={handleTestNotification}>
<Send size={14} />
<Typography.Text>Test Notification</Typography.Text>
</Button>
<Button type="primary" onClick={handleSaveAlert}>
<Check size={14} />
<Typography.Text>Save Alert Rule</Typography.Text>
</Button>
</div>
</div>
);
}
export default Footer;

View File

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

View File

@@ -0,0 +1,51 @@
.create-alert-v2-footer {
position: fixed;
bottom: 0;
left: 63px;
right: 0;
background-color: var(--bg-ink-500);
height: 70px;
border-top: 1px solid var(--bg-slate-500);
padding: 16px 24px;
z-index: 1000;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
.button-group {
display: flex;
gap: 12px;
}
.ant-btn {
display: flex;
align-items: center;
gap: 8px;
}
.ant-btn-default {
background-color: var(--bg-slate-500);
border: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-400);
}
}
.lightMode {
.create-alert-v2-footer {
background-color: var(--bg-vanilla-100);
border-top: 1px solid var(--bg-vanilla-300);
.ant-btn-default {
background-color: var(--bg-vanilla-300);
border: 1px solid var(--bg-vanilla-400);
color: var(--bg-ink-400);
}
.ant-btn-primary {
.ant-typography {
color: var(--bg-vanilla-100);
}
}
}
}

View File

@@ -57,23 +57,23 @@ function NotificationMessage(): JSX.Element {
<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.">
{/* <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>
</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">
{/* <div className="notification-message-header-actions">
<Popover content={templateVariableContent}>
<Button type="text">
<Info size={12} />
Variables
</Button>
</Popover>
</div>
</div> */}
</div>
<TextArea
value={notificationSettings.description}

View File

@@ -1,3 +1,4 @@
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useCreateAlertState } from 'container/CreateAlertV2/context';
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
@@ -16,7 +17,7 @@ export interface ChartPreviewProps {
function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
const { currentQuery, panelType, stagedQuery } = useQueryBuilder();
const { thresholdState, alertState } = useCreateAlertState();
const { thresholdState, alertState, setAlertState } = useCreateAlertState();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
@@ -25,14 +26,24 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
const yAxisUnit = alertState.yAxisUnit || '';
const headline = (
<div className="chart-preview-headline">
<PlotTag
queryType={currentQuery.queryType}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
<YAxisUnitSelector
value={alertState.yAxisUnit}
onChange={(value): void => {
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
}}
/>
</div>
);
const renderQBChartPreview = (): JSX.Element => (
<ChartPreviewComponent
headline={
<PlotTag
queryType={currentQuery.queryType}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
}
headline={headline}
name=""
query={stagedQuery}
selectedInterval={globalSelectedInterval}
@@ -47,12 +58,7 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
const renderPromAndChQueryChartPreview = (): JSX.Element => (
<ChartPreviewComponent
headline={
<PlotTag
queryType={currentQuery.queryType}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
}
headline={headline}
name="Chart Preview"
query={stagedQuery}
alertDef={alertDef}

View File

@@ -2,7 +2,6 @@ import './styles.scss';
import { Button } from 'antd';
import classNames from 'classnames';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { PANEL_TYPES } from 'constants/queryBuilder';
import QuerySectionComponent from 'container/FormAlertRules/QuerySection';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -16,13 +15,7 @@ import { buildAlertDefForChartPreview } from './utils';
function QuerySection(): JSX.Element {
const { currentQuery, handleRunQuery } = useQueryBuilder();
const {
alertState,
setAlertState,
alertType,
setAlertType,
thresholdState,
} = useCreateAlertState();
const { alertType, setAlertType, thresholdState } = useCreateAlertState();
const alertDef = buildAlertDefForChartPreview({ alertType, thresholdState });
@@ -51,17 +44,8 @@ function QuerySection(): JSX.Element {
return (
<div className="query-section">
<Stepper
stepNumber={1}
label="Define the query you want to set an alert on"
/>
<Stepper stepNumber={1} label="Define the query" />
<ChartPreview alertDef={alertDef} />
<YAxisUnitSelector
value={alertState.yAxisUnit}
onChange={(value): void => {
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
}}
/>
<div className="query-section-tabs">
<div className="query-section-query-actions">
{tabs.map((tab) => (

View File

@@ -135,7 +135,7 @@ describe('QuerySection', () => {
expect(screen.getByTestId('stepper')).toBeInTheDocument();
expect(screen.getByTestId('step-number')).toHaveTextContent('1');
expect(screen.getByTestId('step-label')).toHaveTextContent(
'Define the query you want to set an alert on',
'Define the query',
);
// Check if ChartPreview is rendered

View File

@@ -88,6 +88,14 @@
border: 1px solid var(--bg-slate-500);
.ant-card-body {
background-color: var(--bg-ink-500);
.chart-preview-headline {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
}
}
}
}
@@ -99,3 +107,69 @@
border: 1px solid var(--bg-slate-400);
}
}
.lightMode {
.query-section {
.query-section-tabs {
.query-section-query-actions {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
.explorer-view-option {
border-left: 0.5px solid var(--bg-vanilla-300);
border-bottom: 0.5px solid var(--bg-vanilla-300);
&.active-tab {
background-color: var(--bg-vanilla-100);
&:hover {
background-color: var(--bg-vanilla-100) !important;
}
}
&:disabled {
background-color: var(--bg-vanilla-300);
}
&:hover {
color: var(--bg-ink-400);
}
}
}
}
.y-axis-unit-selector-component {
.ant-select {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
}
}
}
.chart-preview-container {
.alert-chart-container {
.ant-card {
border: 1px solid var(--bg-vanilla-300);
.ant-card-body {
background-color: var(--bg-vanilla-100);
}
.ant-card-body {
.chart-preview-header {
.plot-tag {
background-color: var(--bg-vanilla-300);
color: var(--bg-slate-100);
}
}
}
}
}
}
.alert-query-section-container {
background-color: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
}
}
}

View File

@@ -42,3 +42,22 @@
background-position: center;
margin-left: 8px;
}
.lightMode {
.step-number {
background-color: var(--bg-robin-400);
color: var(--text-slate-400);
}
.step-label {
color: var(--bg-ink-300);
}
.dotted-line {
background-image: radial-gradient(
circle,
var(--bg-ink-200) 1px,
transparent 1px
);
}
}

View File

@@ -21,7 +21,6 @@ import {
export const INITIAL_ALERT_STATE: AlertState = {
name: '',
description: '',
labels: {},
yAxisUnit: undefined,
};
@@ -30,7 +29,7 @@ export const INITIAL_CRITICAL_THRESHOLD: Threshold = {
id: v4(),
label: 'CRITICAL',
thresholdValue: 0,
recoveryThresholdValue: 0,
recoveryThresholdValue: null,
unit: '',
channels: [],
color: Color.BG_SAKURA_500,
@@ -40,7 +39,7 @@ export const INITIAL_WARNING_THRESHOLD: Threshold = {
id: v4(),
label: 'WARNING',
thresholdValue: 0,
recoveryThresholdValue: 0,
recoveryThresholdValue: null,
unit: '',
channels: [],
color: Color.BG_AMBER_500,
@@ -50,7 +49,7 @@ export const INITIAL_INFO_THRESHOLD: Threshold = {
id: v4(),
label: 'INFO',
thresholdValue: 0,
recoveryThresholdValue: 0,
recoveryThresholdValue: null,
unit: '',
channels: [],
color: Color.BG_ROBIN_500,
@@ -60,7 +59,7 @@ export const INITIAL_RANDOM_THRESHOLD: Threshold = {
id: v4(),
label: '',
thresholdValue: 0,
recoveryThresholdValue: 0,
recoveryThresholdValue: null,
unit: '',
channels: [],
color: getRandomColor(),
@@ -120,10 +119,10 @@ export const INITIAL_EVALUATION_WINDOW_STATE: EvaluationWindowState = {
};
export const THRESHOLD_OPERATOR_OPTIONS = [
{ value: AlertThresholdOperator.IS_ABOVE, label: 'IS ABOVE' },
{ value: AlertThresholdOperator.IS_BELOW, label: 'IS BELOW' },
{ value: AlertThresholdOperator.IS_EQUAL_TO, label: 'IS EQUAL TO' },
{ value: AlertThresholdOperator.IS_NOT_EQUAL_TO, label: 'IS NOT EQUAL TO' },
{ value: AlertThresholdOperator.IS_ABOVE, label: 'ABOVE' },
{ value: AlertThresholdOperator.IS_BELOW, label: 'BELOW' },
{ value: AlertThresholdOperator.IS_EQUAL_TO, label: 'EQUAL TO' },
{ value: AlertThresholdOperator.IS_NOT_EQUAL_TO, label: 'NOT EQUAL TO' },
];
export const ANOMALY_THRESHOLD_OPERATOR_OPTIONS = [

View File

@@ -107,6 +107,25 @@ export function CreateAlertProvider(
});
}, [alertType]);
const discardAlertRule = useCallback(() => {
setAlertState({
type: 'RESET',
});
setThresholdState({
type: 'RESET',
});
setEvaluationWindow({
type: 'RESET',
});
setAdvancedOptions({
type: 'RESET',
});
setNotificationSettings({
type: 'RESET',
});
handleAlertTypeChange(AlertTypes.METRICS_BASED_ALERT);
}, [handleAlertTypeChange]);
const contextValue: ICreateAlertContextProps = useMemo(
() => ({
alertState,
@@ -121,6 +140,7 @@ export function CreateAlertProvider(
setAdvancedOptions,
notificationSettings,
setNotificationSettings,
discardAlertRule,
}),
[
alertState,
@@ -130,6 +150,7 @@ export function CreateAlertProvider(
evaluationWindow,
advancedOptions,
notificationSettings,
discardAlertRule,
],
);

View File

@@ -16,6 +16,7 @@ export interface ICreateAlertContextProps {
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
notificationSettings: NotificationSettingsState;
setNotificationSettings: Dispatch<NotificationSettingsAction>;
discardAlertRule: () => void;
}
export interface ICreateAlertProviderProps {
@@ -31,14 +32,12 @@ export enum AlertCreationStep {
export interface AlertState {
name: string;
description: string;
labels: Labels;
yAxisUnit: string | undefined;
}
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: 'RESET' };
@@ -47,7 +46,7 @@ export interface Threshold {
id: string;
label: string;
thresholdValue: number;
recoveryThresholdValue: number;
recoveryThresholdValue: number | null;
unit: string;
channels: string[];
color: string;

View File

@@ -41,11 +41,6 @@ export const alertCreationReducer = (
...state,
name: action.payload,
};
case 'SET_ALERT_DESCRIPTION':
return {
...state,
description: action.payload,
};
case 'SET_ALERT_LABELS':
return {
...state,

View File

@@ -10,6 +10,7 @@
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
.prom-ql-icon {
height: 14px;

View File

@@ -1,7 +1,7 @@
import './QuerySection.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Tooltip } from 'antd';
import { Button, Tabs, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
@@ -95,6 +95,7 @@ function QuerySection({
<Tooltip title="Query Builder">
<Button className="nav-btns" data-testid="query-builder-tab">
<Atom size={14} />
<Typography.Text>Query Builder</Typography.Text>
</Button>
</Tooltip>
),
@@ -105,6 +106,7 @@ function QuerySection({
<Tooltip title="ClickHouse">
<Button className="nav-btns">
<Terminal size={14} />
<Typography.Text>ClickHouse Query</Typography.Text>
</Button>
</Tooltip>
),
@@ -117,6 +119,7 @@ function QuerySection({
<PromQLIcon
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
/>
<Typography.Text>PromQL</Typography.Text>
</Button>
</Tooltip>
),