mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
chore: add evaluation settings section (#9134)
This commit is contained in:
parent
73ff89a80a
commit
2c59c1196d
@ -6,13 +6,16 @@ import { Activity, ChartLine } from 'lucide-react';
|
|||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
|
||||||
import { useCreateAlertState } from '../context';
|
import { useCreateAlertState } from '../context';
|
||||||
|
import AdvancedOptions from '../EvaluationSettings/AdvancedOptions';
|
||||||
import Stepper from '../Stepper';
|
import Stepper from '../Stepper';
|
||||||
|
import { showCondensedLayout } from '../utils';
|
||||||
import AlertThreshold from './AlertThreshold';
|
import AlertThreshold from './AlertThreshold';
|
||||||
import AnomalyThreshold from './AnomalyThreshold';
|
import AnomalyThreshold from './AnomalyThreshold';
|
||||||
import { ANOMALY_TAB_TOOLTIP, THRESHOLD_TAB_TOOLTIP } from './constants';
|
import { ANOMALY_TAB_TOOLTIP, THRESHOLD_TAB_TOOLTIP } from './constants';
|
||||||
|
|
||||||
function AlertCondition(): JSX.Element {
|
function AlertCondition(): JSX.Element {
|
||||||
const { alertType, setAlertType } = useCreateAlertState();
|
const { alertType, setAlertType } = useCreateAlertState();
|
||||||
|
const showCondensedLayoutFlag = showCondensedLayout();
|
||||||
|
|
||||||
const showMultipleTabs =
|
const showMultipleTabs =
|
||||||
alertType === AlertTypes.ANOMALY_BASED_ALERT ||
|
alertType === AlertTypes.ANOMALY_BASED_ALERT ||
|
||||||
@ -75,6 +78,11 @@ function AlertCondition(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && <AlertThreshold />}
|
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && <AlertThreshold />}
|
||||||
{alertType === AlertTypes.ANOMALY_BASED_ALERT && <AnomalyThreshold />}
|
{alertType === AlertTypes.ANOMALY_BASED_ALERT && <AnomalyThreshold />}
|
||||||
|
{showCondensedLayoutFlag ? (
|
||||||
|
<div className="condensed-advanced-options-container">
|
||||||
|
<AdvancedOptions />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import './styles.scss';
|
|||||||
|
|
||||||
import { Button, Select, Typography } from 'antd';
|
import { Button, Select, Typography } from 'antd';
|
||||||
import getAllChannels from 'api/channels/getAll';
|
import getAllChannels from 'api/channels/getAll';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
@ -17,6 +18,8 @@ import {
|
|||||||
THRESHOLD_MATCH_TYPE_OPTIONS,
|
THRESHOLD_MATCH_TYPE_OPTIONS,
|
||||||
THRESHOLD_OPERATOR_OPTIONS,
|
THRESHOLD_OPERATOR_OPTIONS,
|
||||||
} from '../context/constants';
|
} from '../context/constants';
|
||||||
|
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
|
||||||
|
import { showCondensedLayout } from '../utils';
|
||||||
import ThresholdItem from './ThresholdItem';
|
import ThresholdItem from './ThresholdItem';
|
||||||
import { UpdateThreshold } from './types';
|
import { UpdateThreshold } from './types';
|
||||||
import {
|
import {
|
||||||
@ -37,6 +40,7 @@ function AlertThreshold(): JSX.Element {
|
|||||||
>(['getChannels'], {
|
>(['getChannels'], {
|
||||||
queryFn: () => getAllChannels(),
|
queryFn: () => getAllChannels(),
|
||||||
});
|
});
|
||||||
|
const showCondensedLayoutFlag = showCondensedLayout();
|
||||||
const channels = data?.data || [];
|
const channels = data?.data || [];
|
||||||
|
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
@ -81,8 +85,18 @@ function AlertThreshold(): JSX.Element {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const evaluationWindowContext = showCondensedLayoutFlag ? (
|
||||||
|
<EvaluationSettings />
|
||||||
|
) : (
|
||||||
|
<strong>Evaluation Window.</strong>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="alert-threshold-container">
|
<div
|
||||||
|
className={classNames('alert-threshold-container', {
|
||||||
|
'condensed-alert-threshold-container': showCondensedLayoutFlag,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{/* Main condition sentence */}
|
{/* Main condition sentence */}
|
||||||
<div className="alert-condition-sentences">
|
<div className="alert-condition-sentences">
|
||||||
<div className="alert-condition-sentence">
|
<div className="alert-condition-sentence">
|
||||||
@ -128,7 +142,7 @@ function AlertThreshold(): JSX.Element {
|
|||||||
options={THRESHOLD_MATCH_TYPE_OPTIONS}
|
options={THRESHOLD_MATCH_TYPE_OPTIONS}
|
||||||
/>
|
/>
|
||||||
<Typography.Text className="sentence-text">
|
<Typography.Text className="sentence-text">
|
||||||
during the <strong>Evaluation Window.</strong>
|
during the {evaluationWindowContext}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -84,6 +84,9 @@
|
|||||||
color: var(--text-vanilla-400);
|
color: var(--text-vanilla-400);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-select {
|
.ant-select {
|
||||||
@ -275,3 +278,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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,7 +7,9 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
import AlertCondition from './AlertCondition';
|
import AlertCondition from './AlertCondition';
|
||||||
import { CreateAlertProvider } from './context';
|
import { CreateAlertProvider } from './context';
|
||||||
import CreateAlertHeader from './CreateAlertHeader';
|
import CreateAlertHeader from './CreateAlertHeader';
|
||||||
|
import EvaluationSettings from './EvaluationSettings';
|
||||||
import QuerySection from './QuerySection';
|
import QuerySection from './QuerySection';
|
||||||
|
import { showCondensedLayout } from './utils';
|
||||||
|
|
||||||
function CreateAlertV2({
|
function CreateAlertV2({
|
||||||
initialQuery = initialQueriesMap.metrics,
|
initialQuery = initialQueriesMap.metrics,
|
||||||
@ -16,14 +18,17 @@ function CreateAlertV2({
|
|||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
useShareBuilderUrl({ defaultValue: initialQuery });
|
useShareBuilderUrl({ defaultValue: initialQuery });
|
||||||
|
|
||||||
|
const showCondensedLayoutFlag = showCondensedLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="create-alert-v2-container">
|
<CreateAlertProvider>
|
||||||
<CreateAlertProvider>
|
<div className="create-alert-v2-container">
|
||||||
<CreateAlertHeader />
|
<CreateAlertHeader />
|
||||||
<QuerySection />
|
<QuerySection />
|
||||||
<AlertCondition />
|
<AlertCondition />
|
||||||
</CreateAlertProvider>
|
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
|
||||||
</div>
|
</div>
|
||||||
|
</CreateAlertProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,129 @@
|
|||||||
|
import { Collapse, Input, Select, Typography } 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="Alert when data stops coming"
|
||||||
|
description="Send notification if no data is received for a specified time period."
|
||||||
|
tooltipText="Useful for monitoring data pipelines or services that should continuously send data. For example, alert if no logs are received for 10 minutes"
|
||||||
|
input={
|
||||||
|
<div className="advanced-option-item-input-group">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter tolerance limit..."
|
||||||
|
type="number"
|
||||||
|
style={{ width: 100 }}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AdvancedOptionItem
|
||||||
|
title="Minimum data required"
|
||||||
|
description="Only trigger alert when there are enough data points to make a reliable decision."
|
||||||
|
tooltipText="Prevents false alarms when there's insufficient data. For example, require at least 5 data points before checking if CPU usage is above 80%."
|
||||||
|
input={
|
||||||
|
<div className="advanced-option-item-input-group">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter minimum datapoints..."
|
||||||
|
style={{ width: 100 }}
|
||||||
|
type="number"
|
||||||
|
onChange={(e): void =>
|
||||||
|
setAdvancedOptions({
|
||||||
|
type: 'SET_ENFORCE_MINIMUM_DATAPOINTS',
|
||||||
|
payload: {
|
||||||
|
minimumDatapoints: Number(e.target.value),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={advancedOptions.enforceMinimumDatapoints.minimumDatapoints}
|
||||||
|
/>
|
||||||
|
<Typography.Text>Datapoints</Typography.Text>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<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."
|
||||||
|
input={
|
||||||
|
<div className="advanced-option-item-input-group">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter delay..."
|
||||||
|
style={{ width: 100 }}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdvancedOptions;
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
trigger="click"
|
||||||
|
showArrow={false}
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
<div className="evaluate-alert-conditions-button-left">
|
||||||
|
{getTimeframeText(evaluationWindow)}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Layout consists of only the evaluation window popover
|
||||||
|
if (showCondensedLayoutFlag) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="condensed-evaluation-settings-container"
|
||||||
|
data-testid="condensed-evaluation-settings-container"
|
||||||
|
>
|
||||||
|
{popoverContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout consists of
|
||||||
|
// - Stepper header
|
||||||
|
// - Evaluation window popover
|
||||||
|
// - Advanced options
|
||||||
|
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>Check conditions using data from</Typography.Text>
|
||||||
|
<div className="evaluate-alert-conditions-separator" />
|
||||||
|
{popoverContent}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<AdvancedOptions />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EvaluationSettings;
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
import { Input, Select, Typography } from 'antd';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';
|
||||||
|
import {
|
||||||
|
CUMULATIVE_WINDOW_DESCRIPTION,
|
||||||
|
ROLLING_WINDOW_DESCRIPTION,
|
||||||
|
TIMEZONE_DATA,
|
||||||
|
} from '../constants';
|
||||||
|
import TimeInput from '../TimeInput';
|
||||||
|
import { IEvaluationWindowDetailsProps } from '../types';
|
||||||
|
import { getCumulativeWindowTimeframeText } from '../utils';
|
||||||
|
|
||||||
|
function EvaluationWindowDetails({
|
||||||
|
evaluationWindow,
|
||||||
|
setEvaluationWindow,
|
||||||
|
}: IEvaluationWindowDetailsProps): JSX.Element {
|
||||||
|
const currentHourOptions = useMemo(() => {
|
||||||
|
const options = [];
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
options.push({ label: i.toString(), value: i });
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const currentMonthOptions = useMemo(() => {
|
||||||
|
const options = [];
|
||||||
|
for (let i = 1; i <= 31; i++) {
|
||||||
|
options.push({ label: i.toString(), value: i });
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const displayText = useMemo(() => {
|
||||||
|
if (
|
||||||
|
evaluationWindow.windowType === 'rolling' &&
|
||||||
|
evaluationWindow.timeframe === 'custom'
|
||||||
|
) {
|
||||||
|
return `Last ${evaluationWindow.startingAt.number} ${
|
||||||
|
ADVANCED_OPTIONS_TIME_UNIT_OPTIONS.find(
|
||||||
|
(option) => option.value === evaluationWindow.startingAt.unit,
|
||||||
|
)?.label
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
if (evaluationWindow.windowType === 'cumulative') {
|
||||||
|
return getCumulativeWindowTimeframeText(evaluationWindow);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}, [evaluationWindow]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
evaluationWindow.windowType === 'rolling' &&
|
||||||
|
evaluationWindow.timeframe !== 'custom'
|
||||||
|
) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentHour =
|
||||||
|
evaluationWindow.windowType === 'cumulative' &&
|
||||||
|
evaluationWindow.timeframe === 'currentHour';
|
||||||
|
const isCurrentDay =
|
||||||
|
evaluationWindow.windowType === 'cumulative' &&
|
||||||
|
evaluationWindow.timeframe === 'currentDay';
|
||||||
|
const isCurrentMonth =
|
||||||
|
evaluationWindow.windowType === 'cumulative' &&
|
||||||
|
evaluationWindow.timeframe === 'currentMonth';
|
||||||
|
|
||||||
|
const handleNumberChange = (value: string): void => {
|
||||||
|
setEvaluationWindow({
|
||||||
|
type: 'SET_STARTING_AT',
|
||||||
|
payload: {
|
||||||
|
number: value,
|
||||||
|
time: evaluationWindow.startingAt.time,
|
||||||
|
timezone: evaluationWindow.startingAt.timezone,
|
||||||
|
unit: evaluationWindow.startingAt.unit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeChange = (value: string): void => {
|
||||||
|
setEvaluationWindow({
|
||||||
|
type: 'SET_STARTING_AT',
|
||||||
|
payload: {
|
||||||
|
number: evaluationWindow.startingAt.number,
|
||||||
|
time: value,
|
||||||
|
timezone: evaluationWindow.startingAt.timezone,
|
||||||
|
unit: evaluationWindow.startingAt.unit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnitChange = (value: string): void => {
|
||||||
|
setEvaluationWindow({
|
||||||
|
type: 'SET_STARTING_AT',
|
||||||
|
payload: {
|
||||||
|
number: evaluationWindow.startingAt.number,
|
||||||
|
time: evaluationWindow.startingAt.time,
|
||||||
|
timezone: evaluationWindow.startingAt.timezone,
|
||||||
|
unit: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimezoneChange = (value: string): void => {
|
||||||
|
setEvaluationWindow({
|
||||||
|
type: 'SET_STARTING_AT',
|
||||||
|
payload: {
|
||||||
|
number: evaluationWindow.startingAt.number,
|
||||||
|
time: evaluationWindow.startingAt.time,
|
||||||
|
timezone: value,
|
||||||
|
unit: evaluationWindow.startingAt.unit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isCurrentHour) {
|
||||||
|
return (
|
||||||
|
<div className="evaluation-window-details">
|
||||||
|
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
||||||
|
<Typography.Text>{displayText}</Typography.Text>
|
||||||
|
<div className="select-group">
|
||||||
|
<Typography.Text>STARTING AT MINUTE</Typography.Text>
|
||||||
|
<Select
|
||||||
|
options={currentHourOptions}
|
||||||
|
value={evaluationWindow.startingAt.number || null}
|
||||||
|
onChange={handleNumberChange}
|
||||||
|
placeholder="Select starting at"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCurrentDay) {
|
||||||
|
return (
|
||||||
|
<div className="evaluation-window-details">
|
||||||
|
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
||||||
|
<Typography.Text>{displayText}</Typography.Text>
|
||||||
|
<div className="select-group time-select-group">
|
||||||
|
<Typography.Text>STARTING AT</Typography.Text>
|
||||||
|
<TimeInput
|
||||||
|
value={evaluationWindow.startingAt.time}
|
||||||
|
onChange={handleTimeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="select-group">
|
||||||
|
<Typography.Text>SELECT TIMEZONE</Typography.Text>
|
||||||
|
<Select
|
||||||
|
options={TIMEZONE_DATA}
|
||||||
|
value={evaluationWindow.startingAt.timezone || null}
|
||||||
|
onChange={handleTimezoneChange}
|
||||||
|
placeholder="Select timezone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCurrentMonth) {
|
||||||
|
return (
|
||||||
|
<div className="evaluation-window-details">
|
||||||
|
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
||||||
|
<Typography.Text>{displayText}</Typography.Text>
|
||||||
|
<div className="select-group">
|
||||||
|
<Typography.Text>STARTING ON DAY</Typography.Text>
|
||||||
|
<Select
|
||||||
|
options={currentMonthOptions}
|
||||||
|
value={evaluationWindow.startingAt.number || null}
|
||||||
|
onChange={handleNumberChange}
|
||||||
|
placeholder="Select starting at"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="select-group time-select-group">
|
||||||
|
<Typography.Text>STARTING AT</Typography.Text>
|
||||||
|
<TimeInput
|
||||||
|
value={evaluationWindow.startingAt.time}
|
||||||
|
onChange={handleTimeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="select-group">
|
||||||
|
<Typography.Text>SELECT TIMEZONE</Typography.Text>
|
||||||
|
<Select
|
||||||
|
options={TIMEZONE_DATA}
|
||||||
|
value={evaluationWindow.startingAt.timezone || null}
|
||||||
|
onChange={handleTimezoneChange}
|
||||||
|
placeholder="Select timezone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="evaluation-window-details">
|
||||||
|
<Typography.Text>{ROLLING_WINDOW_DESCRIPTION}</Typography.Text>
|
||||||
|
<Typography.Text>Specify custom duration</Typography.Text>
|
||||||
|
<Typography.Text>{displayText}</Typography.Text>
|
||||||
|
<div className="select-group">
|
||||||
|
<Typography.Text>VALUE</Typography.Text>
|
||||||
|
<Input
|
||||||
|
name="value"
|
||||||
|
type="number"
|
||||||
|
value={evaluationWindow.startingAt.number}
|
||||||
|
onChange={(e): void => handleNumberChange(e.target.value)}
|
||||||
|
placeholder="Enter value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="select-group time-select-group">
|
||||||
|
<Typography.Text>UNIT</Typography.Text>
|
||||||
|
<Select
|
||||||
|
options={ADVANCED_OPTIONS_TIME_UNIT_OPTIONS}
|
||||||
|
value={evaluationWindow.startingAt.unit || null}
|
||||||
|
onChange={handleUnitChange}
|
||||||
|
placeholder="Select unit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EvaluationWindowDetails;
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
import { Button, Typography } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CUMULATIVE_WINDOW_DESCRIPTION,
|
||||||
|
EVALUATION_WINDOW_TIMEFRAME,
|
||||||
|
EVALUATION_WINDOW_TYPE,
|
||||||
|
ROLLING_WINDOW_DESCRIPTION,
|
||||||
|
} from '../constants';
|
||||||
|
import {
|
||||||
|
CumulativeWindowTimeframes,
|
||||||
|
IEvaluationWindowPopoverProps,
|
||||||
|
RollingWindowTimeframes,
|
||||||
|
} from '../types';
|
||||||
|
import EvaluationWindowDetails from './EvaluationWindowDetails';
|
||||||
|
import { useKeyboardNavigationForEvaluationWindowPopover } from './useKeyboardNavigation';
|
||||||
|
|
||||||
|
function EvaluationWindowPopover({
|
||||||
|
evaluationWindow,
|
||||||
|
setEvaluationWindow,
|
||||||
|
}: IEvaluationWindowPopoverProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
containerRef,
|
||||||
|
firstItemRef,
|
||||||
|
} = useKeyboardNavigationForEvaluationWindowPopover({
|
||||||
|
onSelect: (value: string, sectionId: string): void => {
|
||||||
|
if (sectionId === 'window-type') {
|
||||||
|
setEvaluationWindow({
|
||||||
|
type: 'SET_WINDOW_TYPE',
|
||||||
|
payload: value as 'rolling' | 'cumulative',
|
||||||
|
});
|
||||||
|
} else if (sectionId === 'timeframe') {
|
||||||
|
setEvaluationWindow({
|
||||||
|
type: 'SET_TIMEFRAME',
|
||||||
|
payload: value as RollingWindowTimeframes | CumulativeWindowTimeframes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEscape: (): void => {
|
||||||
|
const triggerElement = document.querySelector(
|
||||||
|
'[aria-haspopup="true"]',
|
||||||
|
) as HTMLElement;
|
||||||
|
triggerElement?.focus();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderEvaluationWindowContent = (
|
||||||
|
label: string,
|
||||||
|
contentOptions: Array<{ label: string; value: string }>,
|
||||||
|
currentValue: string,
|
||||||
|
onChange: (value: string) => void,
|
||||||
|
sectionId: string,
|
||||||
|
): JSX.Element => (
|
||||||
|
<div className="evaluation-window-content-item" data-section-id={sectionId}>
|
||||||
|
<Typography.Text className="evaluation-window-content-item-label">
|
||||||
|
{label}
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="evaluation-window-content-list">
|
||||||
|
{contentOptions.map((option, index) => (
|
||||||
|
<div
|
||||||
|
className={classNames('evaluation-window-content-list-item', {
|
||||||
|
active: currentValue === option.value,
|
||||||
|
})}
|
||||||
|
key={option.value}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
data-value={option.value}
|
||||||
|
data-section-id={sectionId}
|
||||||
|
onClick={(): void => onChange(option.value)}
|
||||||
|
onKeyDown={(e): void => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
onChange(option.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={index === 0 ? firstItemRef : undefined}
|
||||||
|
>
|
||||||
|
<Typography.Text>{option.label}</Typography.Text>
|
||||||
|
{currentValue === option.value && <Check size={12} />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSelectionContent = (): JSX.Element => {
|
||||||
|
if (evaluationWindow.windowType === 'rolling') {
|
||||||
|
if (evaluationWindow.timeframe === 'custom') {
|
||||||
|
return (
|
||||||
|
<EvaluationWindowDetails
|
||||||
|
evaluationWindow={evaluationWindow}
|
||||||
|
setEvaluationWindow={setEvaluationWindow}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="selection-content">
|
||||||
|
<Typography.Text>{ROLLING_WINDOW_DESCRIPTION}</Typography.Text>
|
||||||
|
<Button type="link">Read the docs</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
evaluationWindow.windowType === 'cumulative' &&
|
||||||
|
!evaluationWindow.timeframe
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="selection-content">
|
||||||
|
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
||||||
|
<Button type="link">Read the docs</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EvaluationWindowDetails
|
||||||
|
evaluationWindow={evaluationWindow}
|
||||||
|
setEvaluationWindow={setEvaluationWindow}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="evaluation-window-popover"
|
||||||
|
ref={containerRef}
|
||||||
|
role="menu"
|
||||||
|
aria-label="Evaluation window options"
|
||||||
|
>
|
||||||
|
<div className="evaluation-window-content">
|
||||||
|
{renderEvaluationWindowContent(
|
||||||
|
'EVALUATION WINDOW',
|
||||||
|
EVALUATION_WINDOW_TYPE,
|
||||||
|
evaluationWindow.windowType,
|
||||||
|
(value: string): void =>
|
||||||
|
setEvaluationWindow({
|
||||||
|
type: 'SET_WINDOW_TYPE',
|
||||||
|
payload: value as 'rolling' | 'cumulative',
|
||||||
|
}),
|
||||||
|
'window-type',
|
||||||
|
)}
|
||||||
|
{renderEvaluationWindowContent(
|
||||||
|
'TIMEFRAME',
|
||||||
|
EVALUATION_WINDOW_TIMEFRAME[evaluationWindow.windowType],
|
||||||
|
evaluationWindow.timeframe,
|
||||||
|
(value: string): void =>
|
||||||
|
setEvaluationWindow({
|
||||||
|
type: 'SET_TIMEFRAME',
|
||||||
|
payload: value as RollingWindowTimeframes | CumulativeWindowTimeframes,
|
||||||
|
}),
|
||||||
|
'timeframe',
|
||||||
|
)}
|
||||||
|
{renderSelectionContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EvaluationWindowPopover;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import EvaluationWindowPopover from './EvaluationWindowPopover';
|
||||||
|
|
||||||
|
export default EvaluationWindowPopover;
|
||||||
@ -0,0 +1,180 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
interface UseKeyboardNavigationOptions {
|
||||||
|
onSelect?: (value: string, sectionId: string) => void;
|
||||||
|
onEscape?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useKeyboardNavigationForEvaluationWindowPopover = ({
|
||||||
|
onSelect,
|
||||||
|
onEscape,
|
||||||
|
}: UseKeyboardNavigationOptions = {}): {
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
|
firstItemRef: React.RefObject<HTMLDivElement>;
|
||||||
|
} => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const firstItemRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const getFocusableItems = useCallback((): HTMLElement[] => {
|
||||||
|
if (!containerRef.current) return [];
|
||||||
|
|
||||||
|
return Array.from(
|
||||||
|
containerRef.current.querySelectorAll(
|
||||||
|
'.evaluation-window-content-list-item[tabindex="0"]',
|
||||||
|
),
|
||||||
|
) as HTMLElement[];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getInteractiveElements = useCallback((): HTMLElement[] => {
|
||||||
|
if (!containerRef.current) return [];
|
||||||
|
|
||||||
|
const detailsSection = containerRef.current.querySelector(
|
||||||
|
'.evaluation-window-details',
|
||||||
|
);
|
||||||
|
if (!detailsSection) return [];
|
||||||
|
|
||||||
|
return Array.from(
|
||||||
|
detailsSection.querySelectorAll(
|
||||||
|
'input, select, button, [tabindex="0"], [tabindex="-1"]',
|
||||||
|
),
|
||||||
|
) as HTMLElement[];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getCurrentIndex = useCallback((items: HTMLElement[]): number => {
|
||||||
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
|
return items.findIndex((item) => item === activeElement);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const navigateWithinSection = useCallback(
|
||||||
|
(direction: 'up' | 'down'): void => {
|
||||||
|
const items = getFocusableItems();
|
||||||
|
if (items.length === 0) return;
|
||||||
|
|
||||||
|
const currentIndex = getCurrentIndex(items);
|
||||||
|
let nextIndex: number;
|
||||||
|
if (direction === 'down') {
|
||||||
|
nextIndex = (currentIndex + 1) % items.length;
|
||||||
|
} else {
|
||||||
|
nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
items[nextIndex]?.focus();
|
||||||
|
},
|
||||||
|
[getFocusableItems, getCurrentIndex],
|
||||||
|
);
|
||||||
|
|
||||||
|
const navigateToDetails = useCallback((): void => {
|
||||||
|
const interactiveElements = getInteractiveElements();
|
||||||
|
interactiveElements[0]?.focus();
|
||||||
|
}, [getInteractiveElements]);
|
||||||
|
|
||||||
|
const navigateBackToSection = useCallback((): void => {
|
||||||
|
const items = getFocusableItems();
|
||||||
|
items[0]?.focus();
|
||||||
|
}, [getFocusableItems]);
|
||||||
|
|
||||||
|
const navigateBetweenSections = useCallback(
|
||||||
|
(direction: 'left' | 'right'): void => {
|
||||||
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
|
const isInDetails = activeElement?.closest('.evaluation-window-details');
|
||||||
|
|
||||||
|
if (isInDetails && direction === 'left') {
|
||||||
|
navigateBackToSection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = getFocusableItems();
|
||||||
|
if (items.length === 0) return;
|
||||||
|
|
||||||
|
const currentIndex = getCurrentIndex(items);
|
||||||
|
const DATA_ATTR = 'data-section-id';
|
||||||
|
const currentSectionId = items[currentIndex]?.getAttribute(DATA_ATTR);
|
||||||
|
|
||||||
|
if (currentSectionId === 'window-type' && direction === 'right') {
|
||||||
|
const timeframeItem = items.find(
|
||||||
|
(item) => item.getAttribute(DATA_ATTR) === 'timeframe',
|
||||||
|
);
|
||||||
|
timeframeItem?.focus();
|
||||||
|
} else if (currentSectionId === 'timeframe' && direction === 'left') {
|
||||||
|
const windowTypeItem = items.find(
|
||||||
|
(item) => item.getAttribute(DATA_ATTR) === 'window-type',
|
||||||
|
);
|
||||||
|
windowTypeItem?.focus();
|
||||||
|
} else if (currentSectionId === 'timeframe' && direction === 'right') {
|
||||||
|
navigateToDetails();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
navigateBackToSection,
|
||||||
|
navigateToDetails,
|
||||||
|
getFocusableItems,
|
||||||
|
getCurrentIndex,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelection = useCallback((): void => {
|
||||||
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
|
if (!activeElement || !onSelect) return;
|
||||||
|
|
||||||
|
const value = activeElement.getAttribute('data-value');
|
||||||
|
const sectionId = activeElement.getAttribute('data-section-id');
|
||||||
|
|
||||||
|
if (value && sectionId) {
|
||||||
|
onSelect(value, sectionId);
|
||||||
|
}
|
||||||
|
}, [onSelect]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent): void => {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
navigateWithinSection('down');
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
navigateWithinSection('up');
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
navigateBetweenSections('left');
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
|
navigateBetweenSections('right');
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
event.preventDefault();
|
||||||
|
handleSelection();
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
event.preventDefault();
|
||||||
|
onEscape?.();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[navigateWithinSection, navigateBetweenSections, handleSelection, onEscape],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect((): (() => void) | undefined => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return undefined;
|
||||||
|
|
||||||
|
container.addEventListener('keydown', handleKeyDown);
|
||||||
|
return (): void => container.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [handleKeyDown]);
|
||||||
|
|
||||||
|
useEffect((): void => {
|
||||||
|
if (firstItemRef.current) {
|
||||||
|
firstItemRef.current.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
containerRef: containerRef as React.RefObject<HTMLDivElement>,
|
||||||
|
firstItemRef: firstItemRef as React.RefObject<HTMLDivElement>,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import * as alertState from 'container/CreateAlertV2/context';
|
||||||
|
|
||||||
|
import AdvancedOptions from '../AdvancedOptions';
|
||||||
|
import { createMockAlertContextState } from './testUtils';
|
||||||
|
|
||||||
|
const mockSetAdvancedOptions = jest.fn();
|
||||||
|
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue(
|
||||||
|
createMockAlertContextState({
|
||||||
|
setAdvancedOptions: mockSetAdvancedOptions,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ALERT_WHEN_DATA_STOPS_COMING_TEXT = 'Alert when data stops coming';
|
||||||
|
const MINIMUM_DATA_REQUIRED_TEXT = 'Minimum data required';
|
||||||
|
const ACCOUNT_FOR_DATA_DELAY_TEXT = 'Account for data delay';
|
||||||
|
const ADVANCED_OPTION_ITEM_CLASS = '.advanced-option-item';
|
||||||
|
const SWITCH_ROLE_SELECTOR = '[role="switch"]';
|
||||||
|
|
||||||
|
describe('AdvancedOptions', () => {
|
||||||
|
it('should render evaluation cadence and the advanced options minimized by default', () => {
|
||||||
|
render(<AdvancedOptions />);
|
||||||
|
expect(screen.getByText('ADVANCED OPTIONS')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('How often to check')).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByText(ALERT_WHEN_DATA_STOPS_COMING_TEXT),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByText(MINIMUM_DATA_REQUIRED_TEXT),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByText(ACCOUNT_FOR_DATA_DELAY_TEXT),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to expand the advanced options', () => {
|
||||||
|
render(<AdvancedOptions />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText(ALERT_WHEN_DATA_STOPS_COMING_TEXT),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByText(MINIMUM_DATA_REQUIRED_TEXT),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByText(ACCOUNT_FOR_DATA_DELAY_TEXT),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
|
||||||
|
fireEvent.click(collapse);
|
||||||
|
|
||||||
|
expect(screen.getByText('How often to check')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Alert when data stops coming')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Minimum data required')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Account for data delay')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"Alert when data stops coming" works as expected', () => {
|
||||||
|
render(<AdvancedOptions />);
|
||||||
|
|
||||||
|
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
|
||||||
|
fireEvent.click(collapse);
|
||||||
|
|
||||||
|
const alertWhenDataStopsComingContainer = screen
|
||||||
|
.getByText(ALERT_WHEN_DATA_STOPS_COMING_TEXT)
|
||||||
|
.closest(ADVANCED_OPTION_ITEM_CLASS);
|
||||||
|
const alertWhenDataStopsComingSwitch = alertWhenDataStopsComingContainer?.querySelector(
|
||||||
|
SWITCH_ROLE_SELECTOR,
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
fireEvent.click(alertWhenDataStopsComingSwitch);
|
||||||
|
|
||||||
|
const toleranceInput = screen.getByPlaceholderText(
|
||||||
|
'Enter tolerance limit...',
|
||||||
|
);
|
||||||
|
fireEvent.change(toleranceInput, { target: { value: '10' } });
|
||||||
|
|
||||||
|
expect(mockSetAdvancedOptions).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING',
|
||||||
|
payload: {
|
||||||
|
toleranceLimit: 10,
|
||||||
|
timeUnit: 'min',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"Minimum data required" works as expected', () => {
|
||||||
|
render(<AdvancedOptions />);
|
||||||
|
|
||||||
|
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
|
||||||
|
fireEvent.click(collapse);
|
||||||
|
|
||||||
|
const minimumDataRequiredContainer = screen
|
||||||
|
.getByText(MINIMUM_DATA_REQUIRED_TEXT)
|
||||||
|
.closest(ADVANCED_OPTION_ITEM_CLASS);
|
||||||
|
const minimumDataRequiredSwitch = minimumDataRequiredContainer?.querySelector(
|
||||||
|
SWITCH_ROLE_SELECTOR,
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
fireEvent.click(minimumDataRequiredSwitch);
|
||||||
|
|
||||||
|
const minimumDataRequiredInput = screen.getByPlaceholderText(
|
||||||
|
'Enter minimum datapoints...',
|
||||||
|
);
|
||||||
|
fireEvent.change(minimumDataRequiredInput, { target: { value: '10' } });
|
||||||
|
|
||||||
|
expect(mockSetAdvancedOptions).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_ENFORCE_MINIMUM_DATAPOINTS',
|
||||||
|
payload: {
|
||||||
|
minimumDatapoints: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"Account for data delay" works as expected', () => {
|
||||||
|
render(<AdvancedOptions />);
|
||||||
|
|
||||||
|
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
|
||||||
|
fireEvent.click(collapse);
|
||||||
|
|
||||||
|
const accountForDataDelayContainer = screen
|
||||||
|
.getByText(ACCOUNT_FOR_DATA_DELAY_TEXT)
|
||||||
|
.closest(ADVANCED_OPTION_ITEM_CLASS);
|
||||||
|
const accountForDataDelaySwitch = accountForDataDelayContainer?.querySelector(
|
||||||
|
SWITCH_ROLE_SELECTOR,
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
fireEvent.click(accountForDataDelaySwitch);
|
||||||
|
|
||||||
|
const delayInput = screen.getByPlaceholderText('Enter delay...');
|
||||||
|
fireEvent.change(delayInput, { target: { value: '10' } });
|
||||||
|
|
||||||
|
expect(mockSetAdvancedOptions).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_DELAY_EVALUATION',
|
||||||
|
payload: {
|
||||||
|
delay: 10,
|
||||||
|
timeUnit: 'min',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import * as alertState from 'container/CreateAlertV2/context';
|
||||||
|
import * as utils from 'container/CreateAlertV2/utils';
|
||||||
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
|
||||||
|
import EvaluationSettings from '../EvaluationSettings';
|
||||||
|
import { createMockAlertContextState } from './testUtils';
|
||||||
|
|
||||||
|
const mockSetEvaluationWindow = jest.fn();
|
||||||
|
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue(
|
||||||
|
createMockAlertContextState({
|
||||||
|
setEvaluationWindow: mockSetEvaluationWindow,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('../AdvancedOptions', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (): JSX.Element => (
|
||||||
|
<div data-testid="advanced-options">AdvancedOptions</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const EVALUATION_SETTINGS_TEXT = 'Evaluation settings';
|
||||||
|
const CHECK_CONDITIONS_USING_DATA_FROM_TEXT =
|
||||||
|
'Check conditions using data from';
|
||||||
|
|
||||||
|
describe('EvaluationSettings', () => {
|
||||||
|
it('should render the default evaluation settings layout', () => {
|
||||||
|
render(<EvaluationSettings />);
|
||||||
|
expect(screen.getByText(EVALUATION_SETTINGS_TEXT)).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(CHECK_CONDITIONS_USING_DATA_FROM_TEXT),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('advanced-options')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render evaluation window for anomaly based alert', () => {
|
||||||
|
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValueOnce(
|
||||||
|
createMockAlertContextState({
|
||||||
|
alertType: AlertTypes.ANOMALY_BASED_ALERT,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
render(<EvaluationSettings />);
|
||||||
|
expect(screen.getByText(EVALUATION_SETTINGS_TEXT)).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByText(CHECK_CONDITIONS_USING_DATA_FROM_TEXT),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the condensed evaluation settings layout', () => {
|
||||||
|
jest.spyOn(utils, 'showCondensedLayout').mockReturnValueOnce(true);
|
||||||
|
render(<EvaluationSettings />);
|
||||||
|
// Header, check conditions using data from and advanced options should be hidden
|
||||||
|
expect(screen.queryByText(EVALUATION_SETTINGS_TEXT)).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByText(CHECK_CONDITIONS_USING_DATA_FROM_TEXT),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('advanced-options')).not.toBeInTheDocument();
|
||||||
|
// Only evaluation window popover should be visible
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('condensed-evaluation-settings-container'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,200 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||||
|
|
||||||
|
import EvaluationWindowDetails from '../EvaluationWindowPopover/EvaluationWindowDetails';
|
||||||
|
import { createMockEvaluationWindowState } from './testUtils';
|
||||||
|
|
||||||
|
const mockEvaluationWindowState = createMockEvaluationWindowState();
|
||||||
|
const mockSetEvaluationWindow = jest.fn();
|
||||||
|
|
||||||
|
describe('EvaluationWindowDetails', () => {
|
||||||
|
it('should render the evaluation window details for rolling mode with custom timeframe', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowDetails
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'rolling',
|
||||||
|
timeframe: 'custom',
|
||||||
|
startingAt: {
|
||||||
|
...mockEvaluationWindowState.startingAt,
|
||||||
|
number: '5',
|
||||||
|
unit: UniversalYAxisUnit.MINUTES,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Specify custom duration')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Last 5 Minutes')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the evaluation window details for cumulative mode with current hour', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowDetails
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'cumulative',
|
||||||
|
timeframe: 'currentHour',
|
||||||
|
startingAt: {
|
||||||
|
...mockEvaluationWindowState.startingAt,
|
||||||
|
number: '1',
|
||||||
|
timezone: 'UTC',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText('Current hour, starting at minute 1 (UTC)'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the evaluation window details for cumulative mode with current day', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowDetails
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'cumulative',
|
||||||
|
timeframe: 'currentDay',
|
||||||
|
startingAt: {
|
||||||
|
...mockEvaluationWindowState.startingAt,
|
||||||
|
time: '00:00:00',
|
||||||
|
timezone: 'UTC',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText('Current day, starting from 00:00:00 (UTC)'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the evaluation window details for cumulative mode with current month', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowDetails
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'cumulative',
|
||||||
|
timeframe: 'currentMonth',
|
||||||
|
startingAt: {
|
||||||
|
...mockEvaluationWindowState.startingAt,
|
||||||
|
number: '1',
|
||||||
|
time: '00:00:00',
|
||||||
|
timezone: 'UTC',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText('Current month, starting from day 1 at 00:00:00 (UTC)'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to change the value in rolling mode with custom timeframe', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowDetails
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'rolling',
|
||||||
|
timeframe: 'custom',
|
||||||
|
startingAt: {
|
||||||
|
...mockEvaluationWindowState.startingAt,
|
||||||
|
number: '5',
|
||||||
|
unit: UniversalYAxisUnit.MINUTES,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const valueInput = screen.getByPlaceholderText('Enter value');
|
||||||
|
fireEvent.change(valueInput, { target: { value: '10' } });
|
||||||
|
expect(mockSetEvaluationWindow).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_STARTING_AT',
|
||||||
|
payload: { ...mockEvaluationWindowState.startingAt, number: '10' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to change the value in cumulative mode with current hour', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowDetails
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'cumulative',
|
||||||
|
timeframe: 'currentHour',
|
||||||
|
startingAt: {
|
||||||
|
...mockEvaluationWindowState.startingAt,
|
||||||
|
number: '1',
|
||||||
|
timezone: 'UTC',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectComponent = screen.getByRole('combobox');
|
||||||
|
fireEvent.mouseDown(selectComponent);
|
||||||
|
const option = screen.getByText('10');
|
||||||
|
fireEvent.click(option);
|
||||||
|
expect(mockSetEvaluationWindow).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_STARTING_AT',
|
||||||
|
payload: {
|
||||||
|
...mockEvaluationWindowState.startingAt,
|
||||||
|
number: 10,
|
||||||
|
timezone: 'UTC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to change the value in cumulative mode with current day', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowDetails
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'cumulative',
|
||||||
|
timeframe: 'currentDay',
|
||||||
|
startingAt: {
|
||||||
|
...mockEvaluationWindowState.startingAt,
|
||||||
|
time: '00:00:00',
|
||||||
|
timezone: 'UTC',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const timeInputs = screen.getAllByDisplayValue('00');
|
||||||
|
const hoursInput = timeInputs[0];
|
||||||
|
fireEvent.change(hoursInput, { target: { value: '10' } });
|
||||||
|
expect(mockSetEvaluationWindow).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_STARTING_AT',
|
||||||
|
payload: {
|
||||||
|
...mockEvaluationWindowState.startingAt,
|
||||||
|
time: '10:00:00',
|
||||||
|
timezone: 'UTC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to change the value in cumulative mode with current month', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowDetails
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'cumulative',
|
||||||
|
timeframe: 'currentMonth',
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const comboboxes = screen.getAllByRole('combobox');
|
||||||
|
const daySelectComponent = comboboxes[0];
|
||||||
|
fireEvent.mouseDown(daySelectComponent);
|
||||||
|
const option = screen.getByText('10');
|
||||||
|
fireEvent.click(option);
|
||||||
|
expect(mockSetEvaluationWindow).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_STARTING_AT',
|
||||||
|
payload: { ...mockEvaluationWindowState.startingAt, number: 10 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,298 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { EvaluationWindowState } from 'container/CreateAlertV2/context/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EVALUATION_WINDOW_TIMEFRAME,
|
||||||
|
EVALUATION_WINDOW_TYPE,
|
||||||
|
} from '../constants';
|
||||||
|
import EvaluationWindowPopover from '../EvaluationWindowPopover';
|
||||||
|
import { createMockEvaluationWindowState } from './testUtils';
|
||||||
|
|
||||||
|
const mockEvaluationWindow: EvaluationWindowState = createMockEvaluationWindowState();
|
||||||
|
const mockSetEvaluationWindow = jest.fn();
|
||||||
|
|
||||||
|
const EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS =
|
||||||
|
'.evaluation-window-content-list-item';
|
||||||
|
const EVALUATION_WINDOW_DETAILS_TEST_ID = 'evaluation-window-details';
|
||||||
|
const ENTER_VALUE_PLACEHOLDER = 'Enter value';
|
||||||
|
const EVALUATION_WINDOW_TEXT = 'EVALUATION WINDOW';
|
||||||
|
const LAST_5_MINUTES_TEXT = 'Last 5 minutes';
|
||||||
|
|
||||||
|
jest.mock('../EvaluationWindowPopover/EvaluationWindowDetails', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (): JSX.Element => (
|
||||||
|
<div data-testid={EVALUATION_WINDOW_DETAILS_TEST_ID}>
|
||||||
|
<input placeholder={ENTER_VALUE_PLACEHOLDER} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('EvaluationWindowPopover', () => {
|
||||||
|
it('should render the evaluation window popover with 3 sections', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={mockEvaluationWindow}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText(EVALUATION_WINDOW_TEXT)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render all window type options with rolling selected', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={mockEvaluationWindow}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
EVALUATION_WINDOW_TYPE.forEach((option) => {
|
||||||
|
expect(screen.getByText(option.label)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
const rollingItem = screen
|
||||||
|
.getByText('Rolling')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
expect(rollingItem).toHaveClass('active');
|
||||||
|
|
||||||
|
const cumulativeItem = screen
|
||||||
|
.getByText('Cumulative')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
expect(cumulativeItem).not.toHaveClass('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render all window type options with cumulative selected', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'cumulative',
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
EVALUATION_WINDOW_TYPE.forEach((option) => {
|
||||||
|
expect(screen.getByText(option.label)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const cumulativeItem = screen
|
||||||
|
.getByText('Cumulative')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
expect(cumulativeItem).toHaveClass('active');
|
||||||
|
const rollingItem = screen
|
||||||
|
.getByText('Rolling')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
expect(rollingItem).not.toHaveClass('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render all timeframe options in rolling mode with last 5 minutes selected by default', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={mockEvaluationWindow}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
EVALUATION_WINDOW_TIMEFRAME.rolling.forEach((option) => {
|
||||||
|
expect(screen.getByText(option.label)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
const last5MinutesItem = screen
|
||||||
|
.getByText(LAST_5_MINUTES_TEXT)
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
expect(last5MinutesItem).toHaveClass('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render all timeframe options in cumulative mode with current hour selected by default', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'cumulative',
|
||||||
|
timeframe: 'currentHour',
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
EVALUATION_WINDOW_TIMEFRAME.cumulative.forEach((option) => {
|
||||||
|
expect(screen.getByText(option.label)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
const currentHourItem = screen
|
||||||
|
.getByText('Current hour')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
expect(currentHourItem).toHaveClass('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders help text in details section for rolling mode with non-custom timeframe', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={mockEvaluationWindow}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId(EVALUATION_WINDOW_DETAILS_TEST_ID),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders EvaluationWindowDetails component in details section for rolling mode with custom timeframe', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
timeframe: 'custom',
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText(
|
||||||
|
'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(EVALUATION_WINDOW_DETAILS_TEST_ID),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders EvaluationWindowDetails component in details section for cumulative mode', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={createMockEvaluationWindowState({
|
||||||
|
windowType: 'cumulative',
|
||||||
|
timeframe: 'currentHour',
|
||||||
|
})}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByText(
|
||||||
|
'A Cumulative Window has a fixed starting point and expands over time.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(EVALUATION_WINDOW_DETAILS_TEST_ID),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('keyboard navigation', () => {
|
||||||
|
it('should navigate down through window type options', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={mockEvaluationWindow}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rollingItem = screen
|
||||||
|
.getByText('Rolling')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
rollingItem?.focus();
|
||||||
|
|
||||||
|
fireEvent.keyDown(rollingItem, { key: 'ArrowDown' });
|
||||||
|
const cumulativeItem = screen
|
||||||
|
.getByText('Cumulative')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS);
|
||||||
|
expect(cumulativeItem).toHaveFocus();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate up through window type options', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={mockEvaluationWindow}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const cumulativeItem = screen
|
||||||
|
.getByText('Cumulative')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
cumulativeItem?.focus();
|
||||||
|
|
||||||
|
fireEvent.keyDown(cumulativeItem, { key: 'ArrowUp' });
|
||||||
|
const rollingItem = screen
|
||||||
|
.getByText('Rolling')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS);
|
||||||
|
expect(rollingItem).toHaveFocus();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate right from window type to timeframe', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={mockEvaluationWindow}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rollingItem = screen
|
||||||
|
.getByText('Rolling')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
rollingItem?.focus();
|
||||||
|
|
||||||
|
fireEvent.keyDown(rollingItem, { key: 'ArrowRight' });
|
||||||
|
const timeframeItem = screen
|
||||||
|
.getByText(LAST_5_MINUTES_TEXT)
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS);
|
||||||
|
expect(timeframeItem).toHaveFocus();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate left from timeframe to window type', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={mockEvaluationWindow}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const timeframeItem = screen
|
||||||
|
.getByText(LAST_5_MINUTES_TEXT)
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
timeframeItem?.focus();
|
||||||
|
|
||||||
|
fireEvent.keyDown(timeframeItem, { key: 'ArrowLeft' });
|
||||||
|
const rollingItem = screen
|
||||||
|
.getByText('Rolling')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS);
|
||||||
|
expect(rollingItem).toHaveFocus();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select option with Enter key', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={mockEvaluationWindow}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const cumulativeItem = screen
|
||||||
|
.getByText('Cumulative')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
cumulativeItem?.focus();
|
||||||
|
|
||||||
|
fireEvent.keyDown(cumulativeItem, { key: 'Enter' });
|
||||||
|
expect(mockSetEvaluationWindow).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_WINDOW_TYPE',
|
||||||
|
payload: 'cumulative',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select option with Space key', () => {
|
||||||
|
render(
|
||||||
|
<EvaluationWindowPopover
|
||||||
|
evaluationWindow={mockEvaluationWindow}
|
||||||
|
setEvaluationWindow={mockSetEvaluationWindow}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const cumulativeItem = screen
|
||||||
|
.getByText('Cumulative')
|
||||||
|
.closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement;
|
||||||
|
cumulativeItem?.focus();
|
||||||
|
|
||||||
|
fireEvent.keyDown(cumulativeItem, { key: ' ' });
|
||||||
|
expect(mockSetEvaluationWindow).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_WINDOW_TYPE',
|
||||||
|
payload: 'cumulative',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -4,7 +4,10 @@ import {
|
|||||||
INITIAL_ALERT_THRESHOLD_STATE,
|
INITIAL_ALERT_THRESHOLD_STATE,
|
||||||
INITIAL_EVALUATION_WINDOW_STATE,
|
INITIAL_EVALUATION_WINDOW_STATE,
|
||||||
} from 'container/CreateAlertV2/context/constants';
|
} from 'container/CreateAlertV2/context/constants';
|
||||||
import { ICreateAlertContextProps } from 'container/CreateAlertV2/context/types';
|
import {
|
||||||
|
EvaluationWindowState,
|
||||||
|
ICreateAlertContextProps,
|
||||||
|
} from 'container/CreateAlertV2/context/types';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
|
||||||
export const createMockAlertContextState = (
|
export const createMockAlertContextState = (
|
||||||
@ -22,3 +25,10 @@ export const createMockAlertContextState = (
|
|||||||
setEvaluationWindow: jest.fn(),
|
setEvaluationWindow: jest.fn(),
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createMockEvaluationWindowState = (
|
||||||
|
overrides?: Partial<EvaluationWindowState>,
|
||||||
|
): EvaluationWindowState => ({
|
||||||
|
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export const EVALUATION_WINDOW_TIMEFRAME = {
|
|||||||
{ label: 'Last 1 hour', value: '1h0m0s' },
|
{ label: 'Last 1 hour', value: '1h0m0s' },
|
||||||
{ label: 'Last 2 hours', value: '2h0m0s' },
|
{ label: 'Last 2 hours', value: '2h0m0s' },
|
||||||
{ label: 'Last 4 hours', value: '4h0m0s' },
|
{ label: 'Last 4 hours', value: '4h0m0s' },
|
||||||
|
{ label: 'Custom', value: 'custom' },
|
||||||
],
|
],
|
||||||
cumulative: [
|
cumulative: [
|
||||||
{ label: 'Current hour', value: 'currentHour' },
|
{ label: 'Current hour', value: 'currentHour' },
|
||||||
@ -60,3 +61,9 @@ export const TIMEZONE_DATA = generateTimezoneData().map((timezone) => ({
|
|||||||
label: `${timezone.name} (${timezone.offset})`,
|
label: `${timezone.name} (${timezone.offset})`,
|
||||||
value: timezone.value,
|
value: timezone.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const CUMULATIVE_WINDOW_DESCRIPTION =
|
||||||
|
'A Cumulative Window has a fixed starting point and expands over time.';
|
||||||
|
|
||||||
|
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.';
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
import EvaluationSettings from './EvaluationSettings';
|
||||||
|
|
||||||
|
export default EvaluationSettings;
|
||||||
@ -238,7 +238,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-input {
|
.select-group .ant-input:not(.time-input-field) {
|
||||||
background-color: var(--bg-ink-300);
|
background-color: var(--bg-ink-300);
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
|
|||||||
@ -32,8 +32,6 @@ export enum CumulativeWindowTimeframes {
|
|||||||
export interface IEvaluationWindowPopoverProps {
|
export interface IEvaluationWindowPopoverProps {
|
||||||
evaluationWindow: EvaluationWindowState;
|
evaluationWindow: EvaluationWindowState;
|
||||||
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEvaluationWindowDetailsProps {
|
export interface IEvaluationWindowDetailsProps {
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
// UI side feature flag
|
// UI side feature flag
|
||||||
export const showNewCreateAlertsPage = (): boolean =>
|
export const showNewCreateAlertsPage = (): boolean =>
|
||||||
localStorage.getItem('showNewCreateAlertsPage') === 'true';
|
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';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user