Compare commits
5 Commits
SIG-2878
...
evaluation
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96c90440f3 | ||
|
|
4e93d8df57 | ||
|
|
c9568be5d8 | ||
|
|
1c257f3e14 | ||
|
|
ff8ac96d37 |
@@ -1,5 +1,4 @@
|
||||
import './styles.scss';
|
||||
import '../EvaluationSettings/styles.scss';
|
||||
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
@@ -7,16 +6,13 @@ import { Activity, ChartLine } from 'lucide-react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import AdvancedOptions from '../EvaluationSettings/AdvancedOptions';
|
||||
import Stepper from '../Stepper';
|
||||
import { showCondensedLayout } from '../utils';
|
||||
import AlertThreshold from './AlertThreshold';
|
||||
import AnomalyThreshold from './AnomalyThreshold';
|
||||
import { ANOMALY_TAB_TOOLTIP, THRESHOLD_TAB_TOOLTIP } from './constants';
|
||||
|
||||
function AlertCondition(): JSX.Element {
|
||||
const { alertType, setAlertType } = useCreateAlertState();
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
|
||||
const showMultipleTabs =
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT ||
|
||||
@@ -79,11 +75,6 @@ function AlertCondition(): JSX.Element {
|
||||
</div>
|
||||
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && <AlertThreshold />}
|
||||
{alertType === AlertTypes.ANOMALY_BASED_ALERT && <AnomalyThreshold />}
|
||||
{showCondensedLayoutFlag ? (
|
||||
<div className="condensed-advanced-options-container">
|
||||
<AdvancedOptions />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import './styles.scss';
|
||||
import '../EvaluationSettings/styles.scss';
|
||||
|
||||
import { Button, Select, Typography } from 'antd';
|
||||
import getAllChannels from 'api/channels/getAll';
|
||||
import classNames from 'classnames';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useQuery } from 'react-query';
|
||||
@@ -19,8 +17,6 @@ import {
|
||||
THRESHOLD_MATCH_TYPE_OPTIONS,
|
||||
THRESHOLD_OPERATOR_OPTIONS,
|
||||
} from '../context/constants';
|
||||
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
|
||||
import { showCondensedLayout } from '../utils';
|
||||
import ThresholdItem from './ThresholdItem';
|
||||
import { UpdateThreshold } from './types';
|
||||
import {
|
||||
@@ -41,7 +37,6 @@ function AlertThreshold(): JSX.Element {
|
||||
>(['getChannels'], {
|
||||
queryFn: () => getAllChannels(),
|
||||
});
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
const channels = data?.data || [];
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
@@ -86,18 +81,8 @@ function AlertThreshold(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const evaluationWindowContext = showCondensedLayoutFlag ? (
|
||||
<EvaluationSettings />
|
||||
) : (
|
||||
<strong>Evaluation Window.</strong>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('alert-threshold-container', {
|
||||
'condensed-alert-threshold-container': showCondensedLayoutFlag,
|
||||
})}
|
||||
>
|
||||
<div className="alert-threshold-container">
|
||||
{/* Main condition sentence */}
|
||||
<div className="alert-condition-sentences">
|
||||
<div className="alert-condition-sentence">
|
||||
@@ -143,7 +128,7 @@ function AlertThreshold(): JSX.Element {
|
||||
options={THRESHOLD_MATCH_TYPE_OPTIONS}
|
||||
/>
|
||||
<Typography.Text className="sentence-text">
|
||||
during the {evaluationWindowContext}
|
||||
during the <strong>Evaluation Window.</strong>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,11 @@ import { Channels } from 'types/api/channels/getAll';
|
||||
import ThresholdItem from '../ThresholdItem';
|
||||
import { ThresholdItemProps } from '../types';
|
||||
|
||||
// Mock the enableRecoveryThreshold utility
|
||||
jest.mock('../../utils', () => ({
|
||||
enableRecoveryThreshold: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
const TEST_CONSTANTS = {
|
||||
THRESHOLD_ID: 'test-threshold-1',
|
||||
CRITICAL_LABEL: 'CRITICAL',
|
||||
|
||||
@@ -84,9 +84,6 @@
|
||||
color: var(--text-vanilla-400);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
@@ -278,43 +275,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.condensed-alert-threshold-container,
|
||||
.condensed-anomaly-threshold-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.condensed-advanced-options-container {
|
||||
margin-top: 16px;
|
||||
width: fit-parent;
|
||||
}
|
||||
|
||||
.condensed-evaluation-settings-container {
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 240px;
|
||||
justify-content: space-between;
|
||||
background-color: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
|
||||
.evaluate-alert-conditions-button-left {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.evaluate-alert-conditions-button-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--bg-vanilla-400);
|
||||
gap: 8px;
|
||||
|
||||
.evaluate-alert-conditions-button-right-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background-color: var(--bg-slate-400);
|
||||
padding: 1px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import AlertCondition from './AlertCondition';
|
||||
import { CreateAlertProvider } from './context';
|
||||
import CreateAlertHeader from './CreateAlertHeader';
|
||||
import EvaluationSettings from './EvaluationSettings';
|
||||
import QuerySection from './QuerySection';
|
||||
import { showCondensedLayout } from './utils';
|
||||
|
||||
function CreateAlertV2({
|
||||
initialQuery = initialQueriesMap.metrics,
|
||||
@@ -18,17 +16,14 @@ function CreateAlertV2({
|
||||
}): JSX.Element {
|
||||
useShareBuilderUrl({ defaultValue: initialQuery });
|
||||
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
|
||||
return (
|
||||
<CreateAlertProvider>
|
||||
<div className="create-alert-v2-container">
|
||||
<div className="create-alert-v2-container">
|
||||
<CreateAlertProvider>
|
||||
<CreateAlertHeader />
|
||||
<QuerySection />
|
||||
<AlertCondition />
|
||||
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
|
||||
</div>
|
||||
</CreateAlertProvider>
|
||||
</CreateAlertProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Switch, Typography } from 'antd';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { IAdvancedOptionItemProps } from './types';
|
||||
|
||||
function AdvancedOptionItem({
|
||||
title,
|
||||
description,
|
||||
input,
|
||||
}: IAdvancedOptionItemProps): JSX.Element {
|
||||
const [showInput, setShowInput] = useState<boolean>(false);
|
||||
|
||||
const onToggle = (): void => {
|
||||
setShowInput((currentShowInput) => !currentShowInput);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="advanced-option-item">
|
||||
<div className="advanced-option-item-left-content">
|
||||
<Typography.Text className="advanced-option-item-title">
|
||||
{title}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="advanced-option-item-description">
|
||||
{description}
|
||||
</Typography.Text>
|
||||
{showInput && <div className="advanced-option-item-input">{input}</div>}
|
||||
</div>
|
||||
<div className="advanced-option-item-right-content">
|
||||
<Switch onChange={onToggle} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdvancedOptionItem;
|
||||
@@ -1,123 +0,0 @@
|
||||
import { Collapse, Input, Select } from 'antd';
|
||||
import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import AdvancedOptionItem from './AdvancedOptionItem';
|
||||
import EvaluationCadence from './EvaluationCadence';
|
||||
|
||||
function AdvancedOptions(): JSX.Element {
|
||||
const { advancedOptions, setAdvancedOptions } = useCreateAlertState();
|
||||
|
||||
const timeOptions = Y_AXIS_CATEGORIES.find(
|
||||
(category) => category.name === 'Time',
|
||||
)?.units.map((unit) => ({ label: unit.name, value: unit.id }));
|
||||
|
||||
return (
|
||||
<div className="advanced-options-container">
|
||||
<Collapse bordered={false}>
|
||||
<Collapse.Panel header="ADVANCED OPTIONS" key="1">
|
||||
<EvaluationCadence />
|
||||
<AdvancedOptionItem
|
||||
title="Send a notification if data is missing"
|
||||
description="If data is missing for this alert rule for a certain time period, notify in the default notification channel."
|
||||
input={
|
||||
<Input.Group>
|
||||
<Input
|
||||
placeholder="Enter tolerance limit..."
|
||||
type="number"
|
||||
style={{ width: 240 }}
|
||||
onChange={(e): void =>
|
||||
setAdvancedOptions({
|
||||
type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING',
|
||||
payload: {
|
||||
toleranceLimit: Number(e.target.value),
|
||||
timeUnit: advancedOptions.sendNotificationIfDataIsMissing.timeUnit,
|
||||
},
|
||||
})
|
||||
}
|
||||
value={advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit}
|
||||
/>
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
options={timeOptions}
|
||||
placeholder="Select time unit"
|
||||
onChange={(value): void =>
|
||||
setAdvancedOptions({
|
||||
type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING',
|
||||
payload: {
|
||||
toleranceLimit:
|
||||
advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit,
|
||||
timeUnit: value as string,
|
||||
},
|
||||
})
|
||||
}
|
||||
value={advancedOptions.sendNotificationIfDataIsMissing.timeUnit}
|
||||
/>
|
||||
</Input.Group>
|
||||
}
|
||||
/>
|
||||
<AdvancedOptionItem
|
||||
title="Enforce minimum datapoints"
|
||||
description="Run alert evaluation only when there are minimum of pre-defined number of data points in each result group"
|
||||
input={
|
||||
<Input
|
||||
placeholder="Enter minimum datapoints..."
|
||||
style={{ width: 360 }}
|
||||
type="number"
|
||||
onChange={(e): void =>
|
||||
setAdvancedOptions({
|
||||
type: 'SET_ENFORCE_MINIMUM_DATAPOINTS',
|
||||
payload: {
|
||||
minimumDatapoints: Number(e.target.value),
|
||||
},
|
||||
})
|
||||
}
|
||||
value={advancedOptions.enforceMinimumDatapoints.minimumDatapoints}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<AdvancedOptionItem
|
||||
title="Delay evaluation"
|
||||
description="Delay the evaluation of newer groups to prevent noisy alerts."
|
||||
input={
|
||||
<Input.Group>
|
||||
<Input
|
||||
placeholder="Enter delay..."
|
||||
style={{ width: 240 }}
|
||||
type="number"
|
||||
onChange={(e): void =>
|
||||
setAdvancedOptions({
|
||||
type: 'SET_DELAY_EVALUATION',
|
||||
payload: {
|
||||
delay: Number(e.target.value),
|
||||
timeUnit: advancedOptions.delayEvaluation.timeUnit,
|
||||
},
|
||||
})
|
||||
}
|
||||
value={advancedOptions.delayEvaluation.delay}
|
||||
/>
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
options={timeOptions}
|
||||
placeholder="Select time unit"
|
||||
onChange={(value): void =>
|
||||
setAdvancedOptions({
|
||||
type: 'SET_DELAY_EVALUATION',
|
||||
payload: {
|
||||
delay: advancedOptions.delayEvaluation.delay,
|
||||
timeUnit: value as string,
|
||||
},
|
||||
})
|
||||
}
|
||||
value={advancedOptions.delayEvaluation.timeUnit}
|
||||
/>
|
||||
</Input.Group>
|
||||
}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdvancedOptions;
|
||||
@@ -1,543 +0,0 @@
|
||||
import { Button, DatePicker, Input, Select, Typography } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Calendar,
|
||||
Calendar1,
|
||||
Code,
|
||||
Edit,
|
||||
Edit3Icon,
|
||||
Info,
|
||||
Plus,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import {
|
||||
ADVANCED_OPTIONS_TIME_UNIT_OPTIONS,
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
} from '../context/constants';
|
||||
import { AdvancedOptionsState } from '../context/types';
|
||||
import {
|
||||
EVALUATION_CADENCE_REPEAT_EVERY_MONTH_OPTIONS,
|
||||
EVALUATION_CADENCE_REPEAT_EVERY_OPTIONS,
|
||||
EVALUATION_CADENCE_REPEAT_EVERY_WEEK_OPTIONS,
|
||||
} from './constants';
|
||||
import TimeInput from './TimeInput';
|
||||
import { IEvaluationCadenceDetailsProps } from './types';
|
||||
import {
|
||||
buildAlertScheduleFromCustomSchedule,
|
||||
buildAlertScheduleFromRRule,
|
||||
isValidRRule,
|
||||
TIMEZONE_DATA,
|
||||
} from './utils';
|
||||
|
||||
export function EvaluationCadenceDetails({
|
||||
setIsOpen,
|
||||
}: IEvaluationCadenceDetailsProps): JSX.Element {
|
||||
const { advancedOptions, setAdvancedOptions } = useCreateAlertState();
|
||||
const [evaluationCadence, setEvaluationCadence] = useState<
|
||||
AdvancedOptionsState['evaluationCadence']
|
||||
>({
|
||||
...advancedOptions.evaluationCadence,
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Editor',
|
||||
icon: <Edit3Icon size={14} />,
|
||||
value: 'editor',
|
||||
},
|
||||
{
|
||||
label: 'RRule',
|
||||
icon: <Code size={14} />,
|
||||
value: 'rrule',
|
||||
},
|
||||
];
|
||||
const [activeTab, setActiveTab] = useState<'editor' | 'rrule'>(() =>
|
||||
evaluationCadence.mode === 'custom' ? 'editor' : 'rrule',
|
||||
);
|
||||
|
||||
const occurenceOptions =
|
||||
evaluationCadence.custom.repeatEvery === 'week'
|
||||
? EVALUATION_CADENCE_REPEAT_EVERY_WEEK_OPTIONS
|
||||
: EVALUATION_CADENCE_REPEAT_EVERY_MONTH_OPTIONS;
|
||||
|
||||
const EditorView = (
|
||||
<div className="editor-view" data-testid="editor-view">
|
||||
<div className="select-group">
|
||||
<Typography.Text>REPEAT EVERY</Typography.Text>
|
||||
<Select
|
||||
options={EVALUATION_CADENCE_REPEAT_EVERY_OPTIONS}
|
||||
value={evaluationCadence.custom.repeatEvery || null}
|
||||
onChange={(value): void =>
|
||||
setEvaluationCadence({
|
||||
...evaluationCadence,
|
||||
custom: {
|
||||
...evaluationCadence.custom,
|
||||
repeatEvery: value,
|
||||
occurence: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="Select repeat every"
|
||||
/>
|
||||
</div>
|
||||
<div className="select-group">
|
||||
<Typography.Text>ON DAY(S)</Typography.Text>
|
||||
<Select
|
||||
options={occurenceOptions}
|
||||
value={evaluationCadence.custom.occurence || null}
|
||||
mode="multiple"
|
||||
onChange={(value): void =>
|
||||
setEvaluationCadence({
|
||||
...evaluationCadence,
|
||||
custom: {
|
||||
...evaluationCadence.custom,
|
||||
occurence: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="Select day(s)"
|
||||
/>
|
||||
</div>
|
||||
<div className="select-group">
|
||||
<Typography.Text>AT</Typography.Text>
|
||||
<TimeInput
|
||||
value={evaluationCadence.custom.startAt}
|
||||
onChange={(value): void =>
|
||||
setEvaluationCadence({
|
||||
...evaluationCadence,
|
||||
custom: {
|
||||
...evaluationCadence.custom,
|
||||
startAt: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="select-group">
|
||||
<Typography.Text>TIMEZONE</Typography.Text>
|
||||
<Select
|
||||
options={TIMEZONE_DATA}
|
||||
value={evaluationCadence.custom.timezone || null}
|
||||
onChange={(value): void =>
|
||||
setEvaluationCadence({
|
||||
...evaluationCadence,
|
||||
custom: {
|
||||
...evaluationCadence.custom,
|
||||
timezone: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="Select timezone"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const RRuleView = (
|
||||
<div className="rrule-view" data-testid="rrule-view">
|
||||
<div className="select-group">
|
||||
<Typography.Text>STARTING ON</Typography.Text>
|
||||
<DatePicker
|
||||
value={evaluationCadence.rrule.date}
|
||||
onChange={(value): void =>
|
||||
setEvaluationCadence({
|
||||
...evaluationCadence,
|
||||
rrule: {
|
||||
...evaluationCadence.rrule,
|
||||
date: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="Select date"
|
||||
/>
|
||||
</div>
|
||||
<div className="select-group">
|
||||
<Typography.Text>AT</Typography.Text>
|
||||
<TimeInput
|
||||
value={evaluationCadence.rrule.startAt}
|
||||
onChange={(value): void =>
|
||||
setEvaluationCadence({
|
||||
...evaluationCadence,
|
||||
rrule: {
|
||||
...evaluationCadence.rrule,
|
||||
startAt: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<TextArea
|
||||
value={evaluationCadence.rrule.rrule}
|
||||
placeholder="Enter RRule"
|
||||
onChange={(value): void =>
|
||||
setEvaluationCadence({
|
||||
...evaluationCadence,
|
||||
rrule: {
|
||||
...evaluationCadence.rrule,
|
||||
rrule: value.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleDiscard = (): void => {
|
||||
setIsOpen(false);
|
||||
setAdvancedOptions({
|
||||
type: 'SET_EVALUATION_CADENCE_MODE',
|
||||
payload: 'default',
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveCustomSchedule = (): void => {
|
||||
setAdvancedOptions({
|
||||
type: 'SET_EVALUATION_CADENCE',
|
||||
payload: {
|
||||
...advancedOptions.evaluationCadence,
|
||||
custom: evaluationCadence.custom,
|
||||
rrule: evaluationCadence.rrule,
|
||||
},
|
||||
});
|
||||
setAdvancedOptions({
|
||||
type: 'SET_EVALUATION_CADENCE_MODE',
|
||||
payload: evaluationCadence.mode,
|
||||
});
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const disableSaveButton = useMemo(() => {
|
||||
if (activeTab === 'editor') {
|
||||
return (
|
||||
!evaluationCadence.custom.repeatEvery ||
|
||||
!evaluationCadence.custom.occurence.length ||
|
||||
!evaluationCadence.custom.startAt ||
|
||||
!evaluationCadence.custom.timezone
|
||||
);
|
||||
}
|
||||
return (
|
||||
!evaluationCadence.rrule.rrule ||
|
||||
!evaluationCadence.rrule.date ||
|
||||
!evaluationCadence.rrule.startAt ||
|
||||
!isValidRRule(evaluationCadence.rrule.rrule)
|
||||
);
|
||||
}, [evaluationCadence, activeTab]);
|
||||
|
||||
const schedule = useMemo(() => {
|
||||
if (activeTab === 'rrule') {
|
||||
return buildAlertScheduleFromRRule(
|
||||
evaluationCadence.rrule.rrule,
|
||||
evaluationCadence.rrule.date,
|
||||
evaluationCadence.rrule.startAt,
|
||||
15,
|
||||
);
|
||||
}
|
||||
return buildAlertScheduleFromCustomSchedule(
|
||||
evaluationCadence.custom.repeatEvery,
|
||||
evaluationCadence.custom.occurence,
|
||||
evaluationCadence.custom.startAt,
|
||||
evaluationCadence.custom.timezone,
|
||||
15,
|
||||
);
|
||||
}, [evaluationCadence, activeTab]);
|
||||
|
||||
const handleChangeTab = (tab: 'editor' | 'rrule'): void => {
|
||||
setActiveTab(tab);
|
||||
const mode = tab === 'editor' ? 'custom' : 'rrule';
|
||||
setEvaluationCadence({
|
||||
...evaluationCadence,
|
||||
mode,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="evaluation-cadence-details">
|
||||
<Typography.Text className="evaluation-cadence-details-title">
|
||||
Add Custom Schedule
|
||||
</Typography.Text>
|
||||
<div className="evaluation-cadence-details-content">
|
||||
<div className="evaluation-cadence-details-content-row">
|
||||
<div className="query-section-tabs">
|
||||
<div className="query-section-query-actions">
|
||||
{tabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.value}
|
||||
className={classNames('list-view-tab', 'explorer-view-option', {
|
||||
'active-tab': activeTab === tab.value,
|
||||
})}
|
||||
onClick={(): void => {
|
||||
handleChangeTab(tab.value as 'editor' | 'rrule');
|
||||
}}
|
||||
>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === 'editor' && EditorView}
|
||||
{activeTab === 'rrule' && RRuleView}
|
||||
<div className="buttons-row">
|
||||
<Button type="default" onClick={handleDiscard}>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSaveCustomSchedule}
|
||||
disabled={disableSaveButton}
|
||||
>
|
||||
Save Custom Schedule
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="evaluation-cadence-details-content-row">
|
||||
{schedule ? (
|
||||
<div className="schedule-preview">
|
||||
<div className="schedule-preview-header">
|
||||
<Calendar size={16} />
|
||||
<Typography.Text className="schedule-preview-title">
|
||||
Schedule Preview
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="schedule-preview-list">
|
||||
{schedule.map((date) => (
|
||||
<div key={date.toISOString()} className="schedule-preview-item">
|
||||
<div className="schedule-preview-timeline">
|
||||
<div className="schedule-preview-timeline-line" />
|
||||
</div>
|
||||
<div className="schedule-preview-content">
|
||||
<div className="schedule-preview-date">
|
||||
{date.toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})}
|
||||
,{' '}
|
||||
{date.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}
|
||||
</div>
|
||||
<div className="schedule-preview-separator" />
|
||||
<div className="schedule-preview-timezone">
|
||||
UTC {date.getTimezoneOffset() <= 0 ? '+' : '-'}{' '}
|
||||
{Math.abs(Math.floor(date.getTimezoneOffset() / 60))}:
|
||||
{String(Math.abs(date.getTimezoneOffset() % 60)).padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-schedule">
|
||||
<Info size={32} />
|
||||
<Typography.Text>
|
||||
Please fill the relevant information to generate a schedule
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EditCustomSchedule({
|
||||
setIsEvaluationCadenceDetailsVisible,
|
||||
}: {
|
||||
setIsEvaluationCadenceDetailsVisible: (isOpen: boolean) => void;
|
||||
}): JSX.Element {
|
||||
const { advancedOptions, setAdvancedOptions } = useCreateAlertState();
|
||||
|
||||
const displayText = useMemo(() => {
|
||||
if (advancedOptions.evaluationCadence.mode === 'custom') {
|
||||
return (
|
||||
<Typography.Text>
|
||||
<Typography.Text>Every</Typography.Text>
|
||||
<Typography.Text className="highlight">
|
||||
{advancedOptions.evaluationCadence.custom.repeatEvery
|
||||
.charAt(0)
|
||||
.toUpperCase() +
|
||||
advancedOptions.evaluationCadence.custom.repeatEvery.slice(1)}
|
||||
</Typography.Text>
|
||||
<Typography.Text>on</Typography.Text>
|
||||
<Typography.Text className="highlight">
|
||||
{advancedOptions.evaluationCadence.custom.occurence
|
||||
.map(
|
||||
(occurence) => occurence.charAt(0).toUpperCase() + occurence.slice(1),
|
||||
)
|
||||
.join(', ')}
|
||||
</Typography.Text>
|
||||
<Typography.Text>at</Typography.Text>
|
||||
<Typography.Text className="highlight">
|
||||
{advancedOptions.evaluationCadence.custom.startAt}
|
||||
</Typography.Text>
|
||||
</Typography.Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Typography.Text>
|
||||
<Typography.Text>Starting on</Typography.Text>
|
||||
<Typography.Text className="highlight">
|
||||
{advancedOptions.evaluationCadence.rrule.date?.format('DD/MM/YYYY')}
|
||||
</Typography.Text>
|
||||
<Typography.Text>at</Typography.Text>
|
||||
<Typography.Text className="highlight">
|
||||
{advancedOptions.evaluationCadence.rrule.startAt}
|
||||
</Typography.Text>
|
||||
</Typography.Text>
|
||||
);
|
||||
}, [advancedOptions.evaluationCadence]);
|
||||
|
||||
const handlePreviewAndEdit = (): void => {
|
||||
setIsEvaluationCadenceDetailsVisible(true);
|
||||
};
|
||||
|
||||
const handleDiscard = (): void => {
|
||||
setIsEvaluationCadenceDetailsVisible(false);
|
||||
setAdvancedOptions({
|
||||
type: 'SET_EVALUATION_CADENCE',
|
||||
payload: INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
|
||||
});
|
||||
setAdvancedOptions({
|
||||
type: 'SET_EVALUATION_CADENCE_MODE',
|
||||
payload: 'default',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="edit-custom-schedule">
|
||||
{displayText}
|
||||
<div className="button-row">
|
||||
<Button.Group>
|
||||
<Button type="default" onClick={handlePreviewAndEdit}>
|
||||
<Edit size={12} />
|
||||
<Typography.Text>Edit custom schedule</Typography.Text>
|
||||
</Button>
|
||||
<Button type="default" onClick={handlePreviewAndEdit}>
|
||||
<Calendar1 size={12} />
|
||||
<Typography.Text>Preview</Typography.Text>
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="discard-button"
|
||||
type="default"
|
||||
onClick={handleDiscard}
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EvaluationCadence(): JSX.Element {
|
||||
const [
|
||||
isEvaluationCadenceDetailsVisible,
|
||||
setIsEvaluationCadenceDetailsVisible,
|
||||
] = useState(false);
|
||||
const { advancedOptions, setAdvancedOptions } = useCreateAlertState();
|
||||
|
||||
const showCustomScheduleButton = useMemo(
|
||||
() =>
|
||||
!isEvaluationCadenceDetailsVisible &&
|
||||
advancedOptions.evaluationCadence.mode === 'default',
|
||||
[isEvaluationCadenceDetailsVisible, advancedOptions.evaluationCadence.mode],
|
||||
);
|
||||
|
||||
const showCustomSchedule = (): void => {
|
||||
setIsEvaluationCadenceDetailsVisible(true);
|
||||
setAdvancedOptions({
|
||||
type: 'SET_EVALUATION_CADENCE_MODE',
|
||||
payload: 'custom',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="evaluation-cadence-container">
|
||||
<div className="advanced-option-item evaluation-cadence-item">
|
||||
<div className="advanced-option-item-left-content">
|
||||
<Typography.Text className="advanced-option-item-title">
|
||||
Evaluation cadence
|
||||
</Typography.Text>
|
||||
<Typography.Text className="advanced-option-item-description">
|
||||
Customize when this Alert Rule will run. By default, it runs every 60
|
||||
seconds (1 minute).
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{showCustomScheduleButton && (
|
||||
<div className="advanced-option-item-right-content">
|
||||
<Input.Group className="advanced-option-item-input-group">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter time"
|
||||
style={{ width: 180 }}
|
||||
value={advancedOptions.evaluationCadence.default.value}
|
||||
onChange={(value): void =>
|
||||
setAdvancedOptions({
|
||||
type: 'SET_EVALUATION_CADENCE',
|
||||
payload: {
|
||||
...advancedOptions.evaluationCadence,
|
||||
default: {
|
||||
...advancedOptions.evaluationCadence.default,
|
||||
value: Number(value.target.value),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
options={ADVANCED_OPTIONS_TIME_UNIT_OPTIONS}
|
||||
placeholder="Select time unit"
|
||||
style={{ width: 120 }}
|
||||
value={advancedOptions.evaluationCadence.default.timeUnit}
|
||||
onChange={(value): void =>
|
||||
setAdvancedOptions({
|
||||
type: 'SET_EVALUATION_CADENCE',
|
||||
payload: {
|
||||
...advancedOptions.evaluationCadence,
|
||||
default: {
|
||||
...advancedOptions.evaluationCadence.default,
|
||||
timeUnit: value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Group>
|
||||
<Button
|
||||
className="advanced-option-item-button"
|
||||
onClick={showCustomSchedule}
|
||||
>
|
||||
<Plus size={12} />
|
||||
<Typography.Text>Add custom schedule</Typography.Text>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isEvaluationCadenceDetailsVisible &&
|
||||
advancedOptions.evaluationCadence.mode !== 'default' && (
|
||||
<EditCustomSchedule
|
||||
setIsEvaluationCadenceDetailsVisible={
|
||||
setIsEvaluationCadenceDetailsVisible
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isEvaluationCadenceDetailsVisible && (
|
||||
<EvaluationCadenceDetails
|
||||
isOpen={isEvaluationCadenceDetailsVisible}
|
||||
setIsOpen={setIsEvaluationCadenceDetailsVisible}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EvaluationCadence;
|
||||
@@ -1,85 +0,0 @@
|
||||
import './styles.scss';
|
||||
|
||||
import { Button, Popover, Typography } from 'antd';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import Stepper from '../Stepper';
|
||||
import { showCondensedLayout } from '../utils';
|
||||
import AdvancedOptions from './AdvancedOptions';
|
||||
import EvaluationWindowPopover from './EvaluationWindowPopover';
|
||||
import { getEvaluationWindowTypeText, getTimeframeText } from './utils';
|
||||
|
||||
function EvaluationSettings(): JSX.Element {
|
||||
const {
|
||||
alertType,
|
||||
evaluationWindow,
|
||||
setEvaluationWindow,
|
||||
} = useCreateAlertState();
|
||||
const [
|
||||
isEvaluationWindowPopoverOpen,
|
||||
setIsEvaluationWindowPopoverOpen,
|
||||
] = useState(false);
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
|
||||
const popoverContent = (
|
||||
<Popover
|
||||
open={isEvaluationWindowPopoverOpen}
|
||||
onOpenChange={(visibility: boolean): void => {
|
||||
setIsEvaluationWindowPopoverOpen(visibility);
|
||||
}}
|
||||
content={
|
||||
<EvaluationWindowPopover
|
||||
evaluationWindow={evaluationWindow}
|
||||
setEvaluationWindow={setEvaluationWindow}
|
||||
isOpen={isEvaluationWindowPopoverOpen}
|
||||
setIsOpen={setIsEvaluationWindowPopoverOpen}
|
||||
/>
|
||||
}
|
||||
trigger="click"
|
||||
showArrow={false}
|
||||
>
|
||||
<Button>
|
||||
<div className="evaluate-alert-conditions-button-left">
|
||||
{getTimeframeText(evaluationWindow.windowType, evaluationWindow.timeframe)}
|
||||
</div>
|
||||
<div className="evaluate-alert-conditions-button-right">
|
||||
<div className="evaluate-alert-conditions-button-right-text">
|
||||
{getEvaluationWindowTypeText(evaluationWindow.windowType)}
|
||||
</div>
|
||||
{isEvaluationWindowPopoverOpen ? (
|
||||
<ChevronUp size={16} />
|
||||
) : (
|
||||
<ChevronDown size={16} />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
if (showCondensedLayoutFlag) {
|
||||
return (
|
||||
<div className="condensed-evaluation-settings-container">
|
||||
{popoverContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="evaluation-settings-container">
|
||||
<Stepper stepNumber={3} label="Evaluation settings" />
|
||||
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && (
|
||||
<div className="evaluate-alert-conditions-container">
|
||||
<Typography.Text>Evaluate Alert Conditions over</Typography.Text>
|
||||
<div className="evaluate-alert-conditions-separator" />
|
||||
{popoverContent}
|
||||
</div>
|
||||
)}
|
||||
<AdvancedOptions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EvaluationSettings;
|
||||
@@ -1,248 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import AdvancedOptionItem from '../AdvancedOptionItem';
|
||||
|
||||
const TEST_INPUT_PLACEHOLDER = 'Test input';
|
||||
const TEST_TITLE = 'Test Title';
|
||||
const TEST_DESCRIPTION = 'Test Description';
|
||||
const TEST_VALUE = 'test value';
|
||||
const FIRST_INPUT_PLACEHOLDER = 'First input';
|
||||
const TEST_INPUT_TEST_ID = 'test-input';
|
||||
|
||||
describe('AdvancedOptionItem', () => {
|
||||
const mockInput = (
|
||||
<input
|
||||
data-testid={TEST_INPUT_TEST_ID}
|
||||
placeholder={TEST_INPUT_PLACEHOLDER}
|
||||
/>
|
||||
);
|
||||
|
||||
const defaultProps = {
|
||||
title: TEST_TITLE,
|
||||
description: TEST_DESCRIPTION,
|
||||
input: mockInput,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render title and description', () => {
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(TEST_TITLE)).toBeInTheDocument();
|
||||
expect(screen.getByText(TEST_DESCRIPTION)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render switch component', () => {
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
expect(switchElement).toBeInTheDocument();
|
||||
expect(switchElement).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should not show input initially', () => {
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId(TEST_INPUT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show input when switch is toggled on', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
await user.click(switchElement);
|
||||
|
||||
expect(switchElement).toBeChecked();
|
||||
expect(screen.getByTestId(TEST_INPUT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide input when switch is toggled off', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
|
||||
// First toggle on
|
||||
await user.click(switchElement);
|
||||
expect(screen.getByTestId(TEST_INPUT_TEST_ID)).toBeInTheDocument();
|
||||
|
||||
// Then toggle off
|
||||
await user.click(switchElement);
|
||||
expect(screen.queryByTestId(TEST_INPUT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle switch state correctly', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
|
||||
// Initial state
|
||||
expect(switchElement).not.toBeChecked();
|
||||
|
||||
// After first click
|
||||
await user.click(switchElement);
|
||||
expect(switchElement).toBeChecked();
|
||||
|
||||
// After second click
|
||||
await user.click(switchElement);
|
||||
expect(switchElement).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should render input with correct props when visible', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
await user.click(switchElement);
|
||||
|
||||
const inputElement = screen.getByTestId(TEST_INPUT_TEST_ID);
|
||||
expect(inputElement).toBeInTheDocument();
|
||||
expect(inputElement).toHaveAttribute('placeholder', TEST_INPUT_PLACEHOLDER);
|
||||
});
|
||||
|
||||
it('should handle multiple toggle operations', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
|
||||
// Toggle on
|
||||
await user.click(switchElement);
|
||||
expect(screen.getByTestId(TEST_INPUT_TEST_ID)).toBeInTheDocument();
|
||||
|
||||
// Toggle off
|
||||
await user.click(switchElement);
|
||||
expect(screen.queryByTestId(TEST_INPUT_TEST_ID)).not.toBeInTheDocument();
|
||||
|
||||
// Toggle on again
|
||||
await user.click(switchElement);
|
||||
expect(screen.getByTestId(TEST_INPUT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should maintain input state when toggling', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
|
||||
// Toggle on and interact with input
|
||||
await user.click(switchElement);
|
||||
const inputElement = screen.getByTestId(TEST_INPUT_TEST_ID);
|
||||
await user.type(inputElement, TEST_VALUE);
|
||||
expect(inputElement).toHaveValue(TEST_VALUE);
|
||||
|
||||
// Toggle off
|
||||
await user.click(switchElement);
|
||||
expect(screen.queryByTestId(TEST_INPUT_TEST_ID)).not.toBeInTheDocument();
|
||||
|
||||
// Toggle back on - input should be recreated (fresh state)
|
||||
await user.click(switchElement);
|
||||
const inputElementAgain = screen.getByTestId(TEST_INPUT_TEST_ID);
|
||||
expect(inputElementAgain).toHaveValue(''); // Fresh input, no previous state
|
||||
});
|
||||
|
||||
it('should render with different title and description', () => {
|
||||
const customTitle = 'Custom Title';
|
||||
const customDescription = 'Custom Description';
|
||||
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={customTitle}
|
||||
description={customDescription}
|
||||
input={defaultProps.input}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(customTitle)).toBeInTheDocument();
|
||||
expect(screen.getByText(customDescription)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with complex input component', async () => {
|
||||
const user = userEvent.setup();
|
||||
const complexInput = (
|
||||
<div data-testid="complex-input">
|
||||
<input placeholder={FIRST_INPUT_PLACEHOLDER} />
|
||||
<select>
|
||||
<option value="option1">Option 1</option>
|
||||
<option value="option2">Option 2</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={complexInput}
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
await user.click(switchElement);
|
||||
|
||||
expect(screen.getByTestId('complex-input')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByPlaceholderText(FIRST_INPUT_PLACEHOLDER),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,193 +0,0 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import store from 'store';
|
||||
|
||||
import { CreateAlertProvider } from '../../context';
|
||||
import {
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
} from '../../context/constants';
|
||||
import AdvancedOptions from '../AdvancedOptions';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock dayjs timezone
|
||||
jest.mock('dayjs', () => {
|
||||
const originalDayjs = jest.requireActual('dayjs');
|
||||
const mockDayjs = jest.fn((date) => originalDayjs(date));
|
||||
Object.assign(mockDayjs, originalDayjs);
|
||||
((mockDayjs as unknown) as { tz: { guess: jest.Mock } }).tz = {
|
||||
guess: jest.fn(() => 'UTC'),
|
||||
};
|
||||
return mockDayjs;
|
||||
});
|
||||
|
||||
// Mock Y_AXIS_CATEGORIES
|
||||
jest.mock('components/YAxisUnitSelector/constants', () => ({
|
||||
Y_AXIS_CATEGORIES: [
|
||||
{
|
||||
name: 'Time',
|
||||
units: [
|
||||
{ name: 'Second', id: 's' },
|
||||
{ name: 'Minute', id: 'm' },
|
||||
{ name: 'Hour', id: 'h' },
|
||||
{ name: 'Day', id: 'd' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// Mock the context
|
||||
const mockSetAdvancedOptions = jest.fn();
|
||||
jest.mock('../../context', () => ({
|
||||
...jest.requireActual('../../context'),
|
||||
useCreateAlertState: (): {
|
||||
advancedOptions: typeof INITIAL_ADVANCED_OPTIONS_STATE;
|
||||
setAdvancedOptions: jest.Mock;
|
||||
evaluationWindow: typeof INITIAL_EVALUATION_WINDOW_STATE;
|
||||
setEvaluationWindow: jest.Mock;
|
||||
} => ({
|
||||
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
setAdvancedOptions: mockSetAdvancedOptions,
|
||||
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
setEvaluationWindow: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock EvaluationCadence component
|
||||
jest.mock('../EvaluationCadence', () => ({
|
||||
__esModule: true,
|
||||
default: function MockEvaluationCadence(): JSX.Element {
|
||||
return (
|
||||
<div data-testid="evaluation-cadence">Evaluation Cadence Component</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const TOLERANCE_LIMIT_PLACEHOLDER = 'Enter tolerance limit...';
|
||||
|
||||
const renderAdvancedOptions = (): void => {
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<CreateAlertProvider>
|
||||
<AdvancedOptions />
|
||||
</CreateAlertProvider>
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('AdvancedOptions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const expandAdvancedOptions = async (
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
): Promise<void> => {
|
||||
const collapseHeader = screen.getByRole('button');
|
||||
await user.click(collapseHeader);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('evaluation-cadence')).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
it('should render and allow expansion of advanced options', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderAdvancedOptions();
|
||||
|
||||
expect(screen.getByText('ADVANCED OPTIONS')).toBeInTheDocument();
|
||||
|
||||
await expandAdvancedOptions(user);
|
||||
|
||||
expect(
|
||||
screen.getByText('Send a notification if data is missing'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Enforce minimum datapoints')).toBeInTheDocument();
|
||||
expect(screen.getByText('Delay evaluation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should enable advanced option inputs when switches are toggled', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderAdvancedOptions();
|
||||
|
||||
await expandAdvancedOptions(user);
|
||||
|
||||
const switches = screen.getAllByRole('switch');
|
||||
|
||||
// Toggle the first switch (send notification)
|
||||
await user.click(switches[0]);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByPlaceholderText(TOLERANCE_LIMIT_PLACEHOLDER),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Toggle the second switch (minimum datapoints)
|
||||
await user.click(switches[1]);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByPlaceholderText('Enter minimum datapoints...'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update advanced options state when user interacts with inputs', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderAdvancedOptions();
|
||||
|
||||
await expandAdvancedOptions(user);
|
||||
|
||||
// Enable send notification option
|
||||
const switches = screen.getAllByRole('switch');
|
||||
await user.click(switches[0]);
|
||||
|
||||
// Wait for tolerance input to appear and test interaction
|
||||
const toleranceInput = await screen.findByPlaceholderText(
|
||||
TOLERANCE_LIMIT_PLACEHOLDER,
|
||||
);
|
||||
await user.clear(toleranceInput);
|
||||
await user.type(toleranceInput, '10');
|
||||
|
||||
const timeUnitSelect = screen.getByRole('combobox');
|
||||
await user.click(timeUnitSelect);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Minute')).toBeInTheDocument();
|
||||
});
|
||||
await user.click(screen.getByText('Minute'));
|
||||
|
||||
// Verify that the state update function was called (testing behavior, not exact values)
|
||||
expect(mockSetAdvancedOptions).toHaveBeenCalled();
|
||||
|
||||
// Verify the function was called with the expected action types
|
||||
const { calls } = mockSetAdvancedOptions.mock;
|
||||
const actionTypes = calls.map((call) => call[0].type);
|
||||
expect(actionTypes).toContain('SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING');
|
||||
});
|
||||
});
|
||||
@@ -1,210 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlertV2/context/constants';
|
||||
|
||||
import * as context from '../../context';
|
||||
import EvaluationCadence, {
|
||||
EvaluationCadenceDetails,
|
||||
} from '../EvaluationCadence';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
const mockSetAdvancedOptions = jest.fn();
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
|
||||
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
setAdvancedOptions: mockSetAdvancedOptions,
|
||||
} as any);
|
||||
|
||||
const EDIT_CUSTOM_SCHEDULE_TEXT = 'Edit custom schedule';
|
||||
const PREVIEW_TEXT = 'Preview';
|
||||
const EVALUATION_CADENCE_TEXT = 'Evaluation cadence';
|
||||
const EVALUATION_CADENCE_DESCRIPTION_TEXT =
|
||||
'Customize when this Alert Rule will run. By default, it runs every 60 seconds (1 minute).';
|
||||
const ADD_CUSTOM_SCHEDULE_TEXT = 'Add custom schedule';
|
||||
const SAVE_CUSTOM_SCHEDULE_TEXT = 'Save Custom Schedule';
|
||||
const DISCARD_TEXT = 'Discard';
|
||||
|
||||
describe('EvaluationCadence', () => {
|
||||
it('should render evaluation cadence component in default mode', () => {
|
||||
render(<EvaluationCadence />);
|
||||
|
||||
expect(screen.getByText(EVALUATION_CADENCE_TEXT)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(EVALUATION_CADENCE_DESCRIPTION_TEXT),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(ADD_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render evaluation cadence component in custom mode', () => {
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
|
||||
advancedOptions: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationCadence: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
|
||||
mode: 'custom',
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
render(<EvaluationCadence />);
|
||||
|
||||
expect(screen.getByText(EVALUATION_CADENCE_TEXT)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(EVALUATION_CADENCE_DESCRIPTION_TEXT),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(EDIT_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(PREVIEW_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render evaluation cadence component in rrule mode', () => {
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
|
||||
advancedOptions: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationCadence: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
|
||||
mode: 'rrule',
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
render(<EvaluationCadence />);
|
||||
|
||||
expect(screen.getByText(EVALUATION_CADENCE_TEXT)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(EVALUATION_CADENCE_DESCRIPTION_TEXT),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(EDIT_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(PREVIEW_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking on discard button should reset the evaluation cadence mode to default', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
|
||||
advancedOptions: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationCadence: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
|
||||
mode: 'custom',
|
||||
},
|
||||
},
|
||||
setAdvancedOptions: mockSetAdvancedOptions,
|
||||
} as any);
|
||||
|
||||
render(<EvaluationCadence />);
|
||||
|
||||
expect(screen.getByText(EVALUATION_CADENCE_TEXT)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(EVALUATION_CADENCE_DESCRIPTION_TEXT),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(EDIT_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(PREVIEW_TEXT)).toBeInTheDocument();
|
||||
|
||||
const discardButton = screen.getByTestId('discard-button');
|
||||
await user.click(discardButton);
|
||||
|
||||
expect(mockSetAdvancedOptions).toHaveBeenCalledWith({
|
||||
type: 'SET_EVALUATION_CADENCE_MODE',
|
||||
payload: 'default',
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking on preview button should open the preview modal', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
|
||||
advancedOptions: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationCadence: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
|
||||
mode: 'custom',
|
||||
},
|
||||
},
|
||||
setAdvancedOptions: mockSetAdvancedOptions,
|
||||
} as any);
|
||||
|
||||
render(<EvaluationCadence />);
|
||||
|
||||
expect(screen.queryByText(SAVE_CUSTOM_SCHEDULE_TEXT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(DISCARD_TEXT)).not.toBeInTheDocument();
|
||||
|
||||
const previewButton = screen.getByText(PREVIEW_TEXT);
|
||||
await user.click(previewButton);
|
||||
|
||||
expect(screen.getByText(SAVE_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(DISCARD_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking on edit custom schedule button should open the edit custom schedule modal', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
|
||||
advancedOptions: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationCadence: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
|
||||
mode: 'custom',
|
||||
},
|
||||
},
|
||||
setAdvancedOptions: mockSetAdvancedOptions,
|
||||
} as any);
|
||||
|
||||
render(<EvaluationCadence />);
|
||||
|
||||
expect(screen.queryByText(SAVE_CUSTOM_SCHEDULE_TEXT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(DISCARD_TEXT)).not.toBeInTheDocument();
|
||||
|
||||
const editCustomScheduleButton = screen.getByText(EDIT_CUSTOM_SCHEDULE_TEXT);
|
||||
await user.click(editCustomScheduleButton);
|
||||
|
||||
expect(screen.getByText(SAVE_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(DISCARD_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const mockSetIsOpen = jest.fn();
|
||||
|
||||
const RULE_VIEW_TEXT = 'RRule';
|
||||
const EDITOR_VIEW_TEST_ID = 'editor-view';
|
||||
const RULE_VIEW_TEST_ID = 'rrule-view';
|
||||
|
||||
describe('EvaluationCadenceDetails', () => {
|
||||
it('should render evaluation cadence details component', () => {
|
||||
render(<EvaluationCadenceDetails isOpen setIsOpen={mockSetIsOpen} />);
|
||||
|
||||
expect(screen.getByText('Add Custom Schedule')).toBeInTheDocument();
|
||||
expect(screen.getByText(SAVE_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(DISCARD_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open the editor tab by default', () => {
|
||||
render(<EvaluationCadenceDetails isOpen setIsOpen={mockSetIsOpen} />);
|
||||
|
||||
expect(screen.getByTestId(EDITOR_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(RULE_VIEW_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open the rrule tab when rrule tab is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<EvaluationCadenceDetails isOpen setIsOpen={mockSetIsOpen} />);
|
||||
|
||||
expect(screen.getByTestId(EDITOR_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(RULE_VIEW_TEST_ID)).not.toBeInTheDocument();
|
||||
|
||||
const rruleTab = screen.getByText(RULE_VIEW_TEXT);
|
||||
await user.click(rruleTab);
|
||||
expect(screen.queryByTestId(EDITOR_VIEW_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(RULE_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import * as context from '../../context';
|
||||
import { INITIAL_EVALUATION_WINDOW_STATE } from '../../context/constants';
|
||||
import EvaluationSettings from '../EvaluationSettings';
|
||||
|
||||
const mockSetEvaluationWindow = jest.fn();
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
|
||||
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
setEvaluationWindow: mockSetEvaluationWindow,
|
||||
} as any);
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'../AdvancedOptions',
|
||||
() =>
|
||||
function MockAdvancedOptions(): JSX.Element {
|
||||
return <div data-testid="advanced-options">Advanced Options</div>;
|
||||
},
|
||||
);
|
||||
|
||||
describe('EvaluationSettings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render evaluation settings container', () => {
|
||||
render(<EvaluationSettings />);
|
||||
expect(screen.getByText('Evaluation settings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render evaluation alert conditions text', () => {
|
||||
render(<EvaluationSettings />);
|
||||
|
||||
expect(
|
||||
screen.getByText('Evaluate Alert Conditions over'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display correct timeframe text for rolling window', () => {
|
||||
render(<EvaluationSettings />);
|
||||
|
||||
expect(screen.getByText('Last 5 minutes')).toBeInTheDocument();
|
||||
expect(screen.getByText('Rolling')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display correct timeframe text for cumulative window', () => {
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
|
||||
evaluationWindow: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentDay',
|
||||
},
|
||||
} as any);
|
||||
render(<EvaluationSettings />);
|
||||
|
||||
expect(screen.getByText('Current day')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cumulative')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -191,7 +191,7 @@ describe('utils', () => {
|
||||
|
||||
// When date is provided, DTSTART is automatically added to the rrule string
|
||||
expect(rrulestr).toHaveBeenCalledWith(
|
||||
expect.stringContaining('DTSTART:20240120T020000Z'),
|
||||
expect.stringMatching(/DTSTART:20240120T\d{6}Z/),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import EvalutationSettings from './EvaluationSettings';
|
||||
|
||||
export default EvalutationSettings;
|
||||
@@ -1,9 +1,3 @@
|
||||
// UI side feature flag
|
||||
export const showNewCreateAlertsPage = (): boolean =>
|
||||
localStorage.getItem('showNewCreateAlertsPage') === 'true';
|
||||
|
||||
// UI side FF to switch between the 2 layouts of the create alert page
|
||||
// Layout 1 - Default layout
|
||||
// Layout 2 - Condensed layout
|
||||
export const showCondensedLayout = (): boolean =>
|
||||
localStorage.getItem('showCondensedLayout') === 'true';
|
||||
|
||||
Reference in New Issue
Block a user