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 { 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 ||
|
||||
@ -75,6 +78,11 @@ 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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import './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';
|
||||
@ -17,6 +18,8 @@ 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 {
|
||||
@ -37,6 +40,7 @@ function AlertThreshold(): JSX.Element {
|
||||
>(['getChannels'], {
|
||||
queryFn: () => getAllChannels(),
|
||||
});
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
const channels = data?.data || [];
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
@ -81,8 +85,18 @@ function AlertThreshold(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const evaluationWindowContext = showCondensedLayoutFlag ? (
|
||||
<EvaluationSettings />
|
||||
) : (
|
||||
<strong>Evaluation Window.</strong>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="alert-threshold-container">
|
||||
<div
|
||||
className={classNames('alert-threshold-container', {
|
||||
'condensed-alert-threshold-container': showCondensedLayoutFlag,
|
||||
})}
|
||||
>
|
||||
{/* Main condition sentence */}
|
||||
<div className="alert-condition-sentences">
|
||||
<div className="alert-condition-sentence">
|
||||
@ -128,7 +142,7 @@ function AlertThreshold(): JSX.Element {
|
||||
options={THRESHOLD_MATCH_TYPE_OPTIONS}
|
||||
/>
|
||||
<Typography.Text className="sentence-text">
|
||||
during the <strong>Evaluation Window.</strong>
|
||||
during the {evaluationWindowContext}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -84,6 +84,9 @@
|
||||
color: var(--text-vanilla-400);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.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 { CreateAlertProvider } from './context';
|
||||
import CreateAlertHeader from './CreateAlertHeader';
|
||||
import EvaluationSettings from './EvaluationSettings';
|
||||
import QuerySection from './QuerySection';
|
||||
import { showCondensedLayout } from './utils';
|
||||
|
||||
function CreateAlertV2({
|
||||
initialQuery = initialQueriesMap.metrics,
|
||||
@ -16,14 +18,17 @@ function CreateAlertV2({
|
||||
}): JSX.Element {
|
||||
useShareBuilderUrl({ defaultValue: initialQuery });
|
||||
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
|
||||
return (
|
||||
<div className="create-alert-v2-container">
|
||||
<CreateAlertProvider>
|
||||
<div className="create-alert-v2-container">
|
||||
<CreateAlertHeader />
|
||||
<QuerySection />
|
||||
<AlertCondition />
|
||||
</CreateAlertProvider>
|
||||
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
|
||||
</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_EVALUATION_WINDOW_STATE,
|
||||
} 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';
|
||||
|
||||
export const createMockAlertContextState = (
|
||||
@ -22,3 +25,10 @@ export const createMockAlertContextState = (
|
||||
setEvaluationWindow: jest.fn(),
|
||||
...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 2 hours', value: '2h0m0s' },
|
||||
{ label: 'Last 4 hours', value: '4h0m0s' },
|
||||
{ label: 'Custom', value: 'custom' },
|
||||
],
|
||||
cumulative: [
|
||||
{ label: 'Current hour', value: 'currentHour' },
|
||||
@ -60,3 +61,9 @@ export const TIMEZONE_DATA = generateTimezoneData().map((timezone) => ({
|
||||
label: `${timezone.name} (${timezone.offset})`,
|
||||
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);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-100);
|
||||
|
||||
@ -32,8 +32,6 @@ export enum CumulativeWindowTimeframes {
|
||||
export interface IEvaluationWindowPopoverProps {
|
||||
evaluationWindow: EvaluationWindowState;
|
||||
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export interface IEvaluationWindowDetailsProps {
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
// 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';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user