chore: add evaluation settings section (#9134)

This commit is contained in:
Amlan Kumar Nandy 2025-09-23 22:36:40 +07:00 committed by GitHub
parent 73ff89a80a
commit 2c59c1196d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1592 additions and 10 deletions

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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>
<CreateAlertProvider>
<div className="create-alert-v2-container">
<CreateAlertHeader />
<QuerySection />
<AlertCondition />
</CreateAlertProvider>
</div>
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
</div>
</CreateAlertProvider>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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>,
};
};

View File

@ -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',
},
});
});
});

View File

@ -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();
});
});

View File

@ -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 },
});
});
});

View File

@ -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',
});
});
});
});

View File

@ -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,
});

View File

@ -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.';

View File

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

View File

@ -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);

View File

@ -32,8 +32,6 @@ export enum CumulativeWindowTimeframes {
export interface IEvaluationWindowPopoverProps {
evaluationWindow: EvaluationWindowState;
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
export interface IEvaluationWindowDetailsProps {

View File

@ -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';