chore: add context and time utils for usage in alerts (#9114)
This commit is contained in:
committed by
GitHub
parent
792d0f3db6
commit
9aacf7f2f5
@@ -139,6 +139,7 @@
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rehype-raw": "7.0.0",
|
||||
"rrule": "2.8.1",
|
||||
"stream": "^0.0.2",
|
||||
"style-loader": "1.3.0",
|
||||
"styled-components": "^5.3.11",
|
||||
|
||||
@@ -119,7 +119,9 @@ const filterAndSortTimezones = (
|
||||
return createTimezoneEntry(normalizedTz, offset);
|
||||
});
|
||||
|
||||
const generateTimezoneData = (includeEtcTimezones = false): Timezone[] => {
|
||||
export const generateTimezoneData = (
|
||||
includeEtcTimezones = false,
|
||||
): Timezone[] => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const allTimezones = (Intl as any).supportedValuesOf('timeZone');
|
||||
const timezones: Timezone[] = [];
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
.time-input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
|
||||
.time-input-field {
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
background-color: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: 'Space Mono', monospace;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-400);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time-input-separator {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0 4px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import './TimeInput.scss';
|
||||
|
||||
import { Input } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export interface TimeInputProps {
|
||||
value?: string; // Format: "HH:MM:SS"
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function TimeInput({
|
||||
value = '00:00:00',
|
||||
onChange,
|
||||
disabled = false,
|
||||
className = '',
|
||||
}: TimeInputProps): JSX.Element {
|
||||
const [hours, setHours] = useState('00');
|
||||
const [minutes, setMinutes] = useState('00');
|
||||
const [seconds, setSeconds] = useState('00');
|
||||
|
||||
// Parse initial value
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const timeParts = value.split(':');
|
||||
if (timeParts.length === 3) {
|
||||
setHours(timeParts[0]);
|
||||
setMinutes(timeParts[1]);
|
||||
setSeconds(timeParts[2]);
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const notifyChange = (h: string, m: string, s: string): void => {
|
||||
const rawValue = `${h}:${m}:${s}`;
|
||||
onChange?.(rawValue);
|
||||
};
|
||||
|
||||
const notifyFormattedChange = (h: string, m: string, s: string): void => {
|
||||
const formattedValue = `${h.padStart(2, '0')}:${m.padStart(
|
||||
2,
|
||||
'0',
|
||||
)}:${s.padStart(2, '0')}`;
|
||||
onChange?.(formattedValue);
|
||||
};
|
||||
|
||||
const handleHoursChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
let newHours = e.target.value.replace(/\D/g, '');
|
||||
|
||||
if (newHours.length > 2) {
|
||||
newHours = newHours.slice(0, 2);
|
||||
}
|
||||
|
||||
if (newHours && parseInt(newHours, 10) > 23) {
|
||||
newHours = '23';
|
||||
}
|
||||
setHours(newHours);
|
||||
notifyChange(newHours, minutes, seconds);
|
||||
};
|
||||
|
||||
const handleMinutesChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
let newMinutes = e.target.value.replace(/\D/g, '');
|
||||
if (newMinutes.length > 2) {
|
||||
newMinutes = newMinutes.slice(0, 2);
|
||||
}
|
||||
if (newMinutes && parseInt(newMinutes, 10) > 59) {
|
||||
newMinutes = '59';
|
||||
}
|
||||
setMinutes(newMinutes);
|
||||
notifyChange(hours, newMinutes, seconds);
|
||||
};
|
||||
|
||||
const handleSecondsChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
let newSeconds = e.target.value.replace(/\D/g, '');
|
||||
if (newSeconds.length > 2) {
|
||||
newSeconds = newSeconds.slice(0, 2);
|
||||
}
|
||||
if (newSeconds && parseInt(newSeconds, 10) > 59) {
|
||||
newSeconds = '59';
|
||||
}
|
||||
setSeconds(newSeconds);
|
||||
notifyChange(hours, minutes, newSeconds);
|
||||
};
|
||||
|
||||
const handleHoursBlur = (): void => {
|
||||
const formattedHours = hours.padStart(2, '0');
|
||||
setHours(formattedHours);
|
||||
notifyFormattedChange(formattedHours, minutes, seconds);
|
||||
};
|
||||
|
||||
const handleMinutesBlur = (): void => {
|
||||
const formattedMinutes = minutes.padStart(2, '0');
|
||||
setMinutes(formattedMinutes);
|
||||
notifyFormattedChange(hours, formattedMinutes, seconds);
|
||||
};
|
||||
|
||||
const handleSecondsBlur = (): void => {
|
||||
const formattedSeconds = seconds.padStart(2, '0');
|
||||
setSeconds(formattedSeconds);
|
||||
notifyFormattedChange(hours, minutes, formattedSeconds);
|
||||
};
|
||||
|
||||
// Helper functions for field navigation
|
||||
const getNextField = (current: string): string => {
|
||||
switch (current) {
|
||||
case 'hours':
|
||||
return 'minutes';
|
||||
case 'minutes':
|
||||
return 'seconds';
|
||||
default:
|
||||
return 'hours';
|
||||
}
|
||||
};
|
||||
|
||||
const getPrevField = (current: string): string => {
|
||||
switch (current) {
|
||||
case 'seconds':
|
||||
return 'minutes';
|
||||
case 'minutes':
|
||||
return 'hours';
|
||||
default:
|
||||
return 'seconds';
|
||||
}
|
||||
};
|
||||
|
||||
// Handle key navigation
|
||||
const handleKeyDown = (
|
||||
e: React.KeyboardEvent<HTMLInputElement>,
|
||||
currentField: 'hours' | 'minutes' | 'seconds',
|
||||
): void => {
|
||||
if (e.key === 'ArrowRight' || e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const nextField = document.querySelector(
|
||||
`[data-field="${getNextField(currentField)}"]`,
|
||||
) as HTMLInputElement;
|
||||
nextField?.focus();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
const prevField = document.querySelector(
|
||||
`[data-field="${getPrevField(currentField)}"]`,
|
||||
) as HTMLInputElement;
|
||||
prevField?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`time-input-container ${className}`}>
|
||||
<Input
|
||||
data-field="hours"
|
||||
value={hours}
|
||||
onChange={handleHoursChange}
|
||||
onBlur={handleHoursBlur}
|
||||
onKeyDown={(e): void => handleKeyDown(e, 'hours')}
|
||||
disabled={disabled}
|
||||
maxLength={2}
|
||||
className="time-input-field"
|
||||
placeholder="00"
|
||||
/>
|
||||
<span className="time-input-separator">:</span>
|
||||
<Input
|
||||
data-field="minutes"
|
||||
value={minutes}
|
||||
onChange={handleMinutesChange}
|
||||
onBlur={handleMinutesBlur}
|
||||
onKeyDown={(e): void => handleKeyDown(e, 'minutes')}
|
||||
disabled={disabled}
|
||||
maxLength={2}
|
||||
className="time-input-field"
|
||||
placeholder="00"
|
||||
/>
|
||||
<span className="time-input-separator">:</span>
|
||||
<Input
|
||||
data-field="seconds"
|
||||
value={seconds}
|
||||
onChange={handleSecondsChange}
|
||||
onBlur={handleSecondsBlur}
|
||||
onKeyDown={(e): void => handleKeyDown(e, 'seconds')}
|
||||
disabled={disabled}
|
||||
maxLength={2}
|
||||
className="time-input-field"
|
||||
placeholder="00"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TimeInput.defaultProps = {
|
||||
value: '00:00:00',
|
||||
onChange: undefined,
|
||||
disabled: false,
|
||||
className: '',
|
||||
};
|
||||
|
||||
export default TimeInput;
|
||||
@@ -0,0 +1,3 @@
|
||||
import TimeInput from './TimeInput';
|
||||
|
||||
export default TimeInput;
|
||||
@@ -0,0 +1,241 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import TimeInput from '../TimeInput/TimeInput';
|
||||
|
||||
describe('TimeInput', () => {
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render with default value', () => {
|
||||
render(<TimeInput />);
|
||||
|
||||
expect(screen.getAllByDisplayValue('00')).toHaveLength(3); // hours, minutes, seconds
|
||||
});
|
||||
|
||||
it('should render with provided value', () => {
|
||||
render(<TimeInput value="12:34:56" />);
|
||||
|
||||
expect(screen.getByDisplayValue('12')).toBeInTheDocument(); // hours
|
||||
expect(screen.getByDisplayValue('34')).toBeInTheDocument(); // minutes
|
||||
expect(screen.getByDisplayValue('56')).toBeInTheDocument(); // seconds
|
||||
});
|
||||
|
||||
it('should handle hours changes', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const hoursInput = screen.getAllByDisplayValue('00')[0];
|
||||
fireEvent.change(hoursInput, { target: { value: '12' } });
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('12:00:00');
|
||||
});
|
||||
|
||||
it('should handle minutes changes', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const minutesInput = screen.getAllByDisplayValue('00')[1];
|
||||
fireEvent.change(minutesInput, { target: { value: '30' } });
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('00:30:00');
|
||||
});
|
||||
|
||||
it('should handle seconds changes', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const secondsInput = screen.getAllByDisplayValue('00')[2];
|
||||
fireEvent.change(secondsInput, { target: { value: '45' } });
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('00:00:45');
|
||||
});
|
||||
|
||||
it('should pad single digits with zeros on blur', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const hoursInput = screen.getAllByDisplayValue('00')[0];
|
||||
fireEvent.change(hoursInput, { target: { value: '5' } });
|
||||
fireEvent.blur(hoursInput);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('05:00:00');
|
||||
});
|
||||
|
||||
it('should filter non-numeric characters', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const hoursInput = screen.getAllByDisplayValue('00')[0];
|
||||
fireEvent.change(hoursInput, { target: { value: '1a2b3c' } });
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('12:00:00');
|
||||
});
|
||||
|
||||
it('should limit input to 2 characters', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const hoursInput = screen.getAllByDisplayValue('00')[0];
|
||||
fireEvent.change(hoursInput, { target: { value: '123456' } });
|
||||
|
||||
expect(hoursInput).toHaveValue('12');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('12:00:00');
|
||||
});
|
||||
|
||||
it('should handle keyboard navigation with ArrowRight', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TimeInput />);
|
||||
|
||||
const hoursInput = screen.getAllByDisplayValue('00')[0];
|
||||
const minutesInput = screen.getAllByDisplayValue('00')[1];
|
||||
|
||||
await user.click(hoursInput);
|
||||
await user.keyboard('{ArrowRight}');
|
||||
|
||||
expect(minutesInput).toHaveFocus();
|
||||
});
|
||||
|
||||
it('should handle keyboard navigation with ArrowLeft', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TimeInput />);
|
||||
|
||||
const hoursInput = screen.getAllByDisplayValue('00')[0];
|
||||
const minutesInput = screen.getAllByDisplayValue('00')[1];
|
||||
|
||||
await user.click(minutesInput);
|
||||
await user.keyboard('{ArrowLeft}');
|
||||
|
||||
expect(hoursInput).toHaveFocus();
|
||||
});
|
||||
|
||||
it('should handle Tab navigation', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TimeInput />);
|
||||
|
||||
const hoursInput = screen.getAllByDisplayValue('00')[0];
|
||||
const minutesInput = screen.getAllByDisplayValue('00')[1];
|
||||
|
||||
await user.click(hoursInput);
|
||||
await user.keyboard('{Tab}');
|
||||
|
||||
expect(minutesInput).toHaveFocus();
|
||||
});
|
||||
|
||||
it('should disable inputs when disabled prop is true', () => {
|
||||
render(<TimeInput disabled />);
|
||||
|
||||
const inputs = screen.getAllByRole('textbox');
|
||||
inputs.forEach((input) => {
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update internal state when value prop changes', () => {
|
||||
const { rerender } = render(<TimeInput value="01:02:03" />);
|
||||
|
||||
expect(screen.getByDisplayValue('01')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('02')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('03')).toBeInTheDocument();
|
||||
|
||||
rerender(<TimeInput value="04:05:06" />);
|
||||
|
||||
expect(screen.getByDisplayValue('04')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('05')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('06')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle partial time values', () => {
|
||||
render(<TimeInput value="12:34" />);
|
||||
|
||||
// Should fall back to default values for incomplete format
|
||||
expect(screen.getAllByDisplayValue('00')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should cap hours at 23 when user enters value > 23', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const hoursInput = screen.getAllByDisplayValue('00')[0];
|
||||
fireEvent.change(hoursInput, { target: { value: '25' } });
|
||||
|
||||
expect(hoursInput).toHaveValue('23');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('23:00:00');
|
||||
});
|
||||
|
||||
it('should cap hours at 23 when user enters value = 24', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const hoursInput = screen.getAllByDisplayValue('00')[0];
|
||||
fireEvent.change(hoursInput, { target: { value: '24' } });
|
||||
|
||||
expect(hoursInput).toHaveValue('23');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('23:00:00');
|
||||
});
|
||||
|
||||
it('should allow hours value of 23', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const hoursInput = screen.getAllByDisplayValue('00')[0];
|
||||
fireEvent.change(hoursInput, { target: { value: '23' } });
|
||||
|
||||
expect(hoursInput).toHaveValue('23');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('23:00:00');
|
||||
});
|
||||
|
||||
it('should cap minutes at 59 when user enters value > 59', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const minutesInput = screen.getAllByDisplayValue('00')[1];
|
||||
fireEvent.change(minutesInput, { target: { value: '65' } });
|
||||
|
||||
expect(minutesInput).toHaveValue('59');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('00:59:00');
|
||||
});
|
||||
|
||||
it('should cap minutes at 59 when user enters value = 60', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const minutesInput = screen.getAllByDisplayValue('00')[1];
|
||||
fireEvent.change(minutesInput, { target: { value: '60' } });
|
||||
|
||||
expect(minutesInput).toHaveValue('59');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('00:59:00');
|
||||
});
|
||||
|
||||
it('should allow minutes value of 59', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const minutesInput = screen.getAllByDisplayValue('00')[1];
|
||||
fireEvent.change(minutesInput, { target: { value: '59' } });
|
||||
|
||||
expect(minutesInput).toHaveValue('59');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('00:59:00');
|
||||
});
|
||||
|
||||
it('should cap seconds at 59 when user enters value > 59', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const secondsInput = screen.getAllByDisplayValue('00')[2];
|
||||
fireEvent.change(secondsInput, { target: { value: '75' } });
|
||||
|
||||
expect(secondsInput).toHaveValue('59');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('00:00:59');
|
||||
});
|
||||
|
||||
it('should cap seconds at 59 when user enters value = 60', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const secondsInput = screen.getAllByDisplayValue('00')[2];
|
||||
fireEvent.change(secondsInput, { target: { value: '60' } });
|
||||
|
||||
expect(secondsInput).toHaveValue('59');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('00:00:59');
|
||||
});
|
||||
|
||||
it('should allow seconds value of 59', () => {
|
||||
render(<TimeInput onChange={mockOnChange} />);
|
||||
|
||||
const secondsInput = screen.getAllByDisplayValue('00')[2];
|
||||
fireEvent.change(secondsInput, { target: { value: '59' } });
|
||||
|
||||
expect(secondsInput).toHaveValue('59');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('00:00:59');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,656 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable import/first */
|
||||
|
||||
// Mock dayjs before importing any other modules
|
||||
const MOCK_DATE_STRING = '2024-01-15T00:30:00Z';
|
||||
const MOCK_DATE_STRING_NON_LEAP_YEAR = '2023-01-15T00:30:00Z';
|
||||
const MOCK_DATE_STRING_SPANS_MONTHS = '2024-01-31T00:30:00Z';
|
||||
const FREQ_DAILY = 'FREQ=DAILY';
|
||||
const TEN_THIRTY_TIME = '10:30:00';
|
||||
const NINE_AM_TIME = '09:00:00';
|
||||
jest.mock('dayjs', () => {
|
||||
const originalDayjs = jest.requireActual('dayjs');
|
||||
const mockDayjs = jest.fn((date?: string | Date) => {
|
||||
if (date) {
|
||||
return originalDayjs(date);
|
||||
}
|
||||
return originalDayjs(MOCK_DATE_STRING);
|
||||
});
|
||||
Object.keys(originalDayjs).forEach((key) => {
|
||||
((mockDayjs as unknown) as Record<string, unknown>)[key] = originalDayjs[key];
|
||||
});
|
||||
return mockDayjs;
|
||||
});
|
||||
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { EvaluationWindowState } from 'container/CreateAlertV2/context/types';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { rrulestr } from 'rrule';
|
||||
|
||||
import { RollingWindowTimeframes } from '../types';
|
||||
import {
|
||||
buildAlertScheduleFromCustomSchedule,
|
||||
buildAlertScheduleFromRRule,
|
||||
getCumulativeWindowTimeframeText,
|
||||
getCustomRollingWindowTimeframeText,
|
||||
getEvaluationWindowTypeText,
|
||||
getRollingWindowTimeframeText,
|
||||
getTimeframeText,
|
||||
isValidRRule,
|
||||
} from '../utils';
|
||||
|
||||
jest.mock('rrule', () => ({
|
||||
rrulestr: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('components/CustomTimePicker/timezoneUtils', () => ({
|
||||
generateTimezoneData: jest.fn().mockReturnValue([
|
||||
{ name: 'UTC', value: 'UTC', offset: '+00:00' },
|
||||
{ name: 'America/New_York', value: 'America/New_York', offset: '-05:00' },
|
||||
{ name: 'Europe/London', value: 'Europe/London', offset: '+00:00' },
|
||||
]),
|
||||
}));
|
||||
|
||||
const mockEvaluationWindowState: EvaluationWindowState = {
|
||||
windowType: 'rolling',
|
||||
timeframe: '5m0s',
|
||||
startingAt: {
|
||||
number: '0',
|
||||
timezone: 'UTC',
|
||||
time: '00:00:00',
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
};
|
||||
|
||||
const formatDate = (date: Date): string =>
|
||||
dayjs(date).format('DD-MM-YYYY HH:mm:ss');
|
||||
|
||||
describe('utils', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getEvaluationWindowTypeText', () => {
|
||||
it('should return correct text for rolling window', () => {
|
||||
expect(getEvaluationWindowTypeText('rolling')).toBe('Rolling');
|
||||
});
|
||||
|
||||
it('should return correct text for cumulative window', () => {
|
||||
expect(getEvaluationWindowTypeText('cumulative')).toBe('Cumulative');
|
||||
});
|
||||
|
||||
it('should default to empty string for unknown type', () => {
|
||||
expect(
|
||||
getEvaluationWindowTypeText('unknown' as 'rolling' | 'cumulative'),
|
||||
).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCumulativeWindowTimeframeText', () => {
|
||||
it('should return correct text for current hour', () => {
|
||||
expect(
|
||||
getCumulativeWindowTimeframeText({
|
||||
...mockEvaluationWindowState,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentHour',
|
||||
}),
|
||||
).toBe('Current hour, starting at minute 0 (UTC)');
|
||||
});
|
||||
|
||||
it('should return correct text for current day', () => {
|
||||
expect(
|
||||
getCumulativeWindowTimeframeText({
|
||||
...mockEvaluationWindowState,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentDay',
|
||||
}),
|
||||
).toBe('Current day, starting from 00:00:00 (UTC)');
|
||||
});
|
||||
|
||||
it('should return correct text for current month', () => {
|
||||
expect(
|
||||
getCumulativeWindowTimeframeText({
|
||||
...mockEvaluationWindowState,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentMonth',
|
||||
}),
|
||||
).toBe('Current month, starting from day 0 at 00:00:00 (UTC)');
|
||||
});
|
||||
|
||||
it('should default to empty string for unknown timeframe', () => {
|
||||
expect(
|
||||
getCumulativeWindowTimeframeText({
|
||||
...mockEvaluationWindowState,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'unknown',
|
||||
}),
|
||||
).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRollingWindowTimeframeText', () => {
|
||||
it('should return correct text for last 5 minutes', () => {
|
||||
expect(
|
||||
getRollingWindowTimeframeText(RollingWindowTimeframes.LAST_5_MINUTES),
|
||||
).toBe('Last 5 minutes');
|
||||
});
|
||||
|
||||
it('should return correct text for last 10 minutes', () => {
|
||||
expect(
|
||||
getRollingWindowTimeframeText(RollingWindowTimeframes.LAST_10_MINUTES),
|
||||
).toBe('Last 10 minutes');
|
||||
});
|
||||
|
||||
it('should return correct text for last 15 minutes', () => {
|
||||
expect(
|
||||
getRollingWindowTimeframeText(RollingWindowTimeframes.LAST_15_MINUTES),
|
||||
).toBe('Last 15 minutes');
|
||||
});
|
||||
|
||||
it('should return correct text for last 30 minutes', () => {
|
||||
expect(
|
||||
getRollingWindowTimeframeText(RollingWindowTimeframes.LAST_30_MINUTES),
|
||||
).toBe('Last 30 minutes');
|
||||
});
|
||||
|
||||
it('should return correct text for last 1 hour', () => {
|
||||
expect(
|
||||
getRollingWindowTimeframeText(RollingWindowTimeframes.LAST_1_HOUR),
|
||||
).toBe('Last 1 hour');
|
||||
});
|
||||
|
||||
it('should return correct text for last 2 hours', () => {
|
||||
expect(
|
||||
getRollingWindowTimeframeText(RollingWindowTimeframes.LAST_2_HOURS),
|
||||
).toBe('Last 2 hours');
|
||||
});
|
||||
|
||||
it('should return correct text for last 4 hours', () => {
|
||||
expect(
|
||||
getRollingWindowTimeframeText(RollingWindowTimeframes.LAST_4_HOURS),
|
||||
).toBe('Last 4 hours');
|
||||
});
|
||||
|
||||
it('should default to Last 5 minutes for unknown timeframe', () => {
|
||||
expect(
|
||||
getRollingWindowTimeframeText('unknown' as RollingWindowTimeframes),
|
||||
).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCustomRollingWindowTimeframeText', () => {
|
||||
it('should return correct text for custom rolling window', () => {
|
||||
expect(getCustomRollingWindowTimeframeText(mockEvaluationWindowState)).toBe(
|
||||
'Last 0 Minutes',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimeframeText', () => {
|
||||
it('should call getCustomRollingWindowTimeframeText for custom rolling window', () => {
|
||||
expect(
|
||||
getTimeframeText({
|
||||
...mockEvaluationWindowState,
|
||||
windowType: 'rolling',
|
||||
timeframe: 'custom',
|
||||
startingAt: {
|
||||
...mockEvaluationWindowState.startingAt,
|
||||
number: '4',
|
||||
},
|
||||
}),
|
||||
).toBe('Last 4 Minutes');
|
||||
});
|
||||
|
||||
it('should call getRollingWindowTimeframeText for rolling window', () => {
|
||||
expect(getTimeframeText(mockEvaluationWindowState)).toBe('Last 5 minutes');
|
||||
});
|
||||
|
||||
it('should call getCumulativeWindowTimeframeText for cumulative window', () => {
|
||||
expect(
|
||||
getTimeframeText({
|
||||
...mockEvaluationWindowState,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentDay',
|
||||
}),
|
||||
).toBe('Current day, starting from 00:00:00 (UTC)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildAlertScheduleFromRRule', () => {
|
||||
const mockRRule = {
|
||||
all: jest.fn((callback) => {
|
||||
const dates = [
|
||||
new Date(MOCK_DATE_STRING),
|
||||
new Date('2024-01-16T10:30:00Z'),
|
||||
new Date('2024-01-17T10:30:00Z'),
|
||||
];
|
||||
dates.forEach((date, index) => callback(date, index));
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(rrulestr as jest.Mock).mockReturnValue(mockRRule);
|
||||
});
|
||||
|
||||
it('should return null for empty rrule string', () => {
|
||||
const result = buildAlertScheduleFromRRule('', null, '10:30:00');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should build schedule from valid rrule string', () => {
|
||||
const result = buildAlertScheduleFromRRule(
|
||||
FREQ_DAILY,
|
||||
null,
|
||||
TEN_THIRTY_TIME,
|
||||
);
|
||||
|
||||
expect(rrulestr).toHaveBeenCalledWith(FREQ_DAILY);
|
||||
expect(result).toEqual([
|
||||
new Date(MOCK_DATE_STRING),
|
||||
new Date('2024-01-16T10:30:00Z'),
|
||||
new Date('2024-01-17T10:30:00Z'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle rrule with DTSTART', () => {
|
||||
const date = dayjs('2024-01-20');
|
||||
buildAlertScheduleFromRRule(FREQ_DAILY, date, NINE_AM_TIME);
|
||||
|
||||
// When date is provided, DTSTART is automatically added to the rrule string
|
||||
expect(rrulestr).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/DTSTART:20240120T\d{6}Z/),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle rrule without DTSTART', () => {
|
||||
// Test with no date provided - should use original rrule string
|
||||
const result = buildAlertScheduleFromRRule(FREQ_DAILY, null, NINE_AM_TIME);
|
||||
|
||||
expect(rrulestr).toHaveBeenCalledWith(FREQ_DAILY);
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should handle escaped newlines', () => {
|
||||
buildAlertScheduleFromRRule('FREQ=DAILY\\nINTERVAL=1', null, '10:30:00');
|
||||
|
||||
expect(rrulestr).toHaveBeenCalledWith('FREQ=DAILY\nINTERVAL=1');
|
||||
});
|
||||
|
||||
it('should limit occurrences to maxOccurrences', () => {
|
||||
const result = buildAlertScheduleFromRRule(FREQ_DAILY, null, '10:30:00', 2);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should return null on error', () => {
|
||||
(rrulestr as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Invalid rrule');
|
||||
});
|
||||
|
||||
const result = buildAlertScheduleFromRRule('INVALID', null, '10:30:00');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildAlertScheduleFromCustomSchedule', () => {
|
||||
it('should generate monthly occurrences', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'month',
|
||||
['1', '15'],
|
||||
'10:30:00',
|
||||
5,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'15-01-2024 10:30:00',
|
||||
'01-02-2024 10:30:00',
|
||||
'15-02-2024 10:30:00',
|
||||
'01-03-2024 10:30:00',
|
||||
'15-03-2024 10:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate weekly occurrences', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'week',
|
||||
['monday', 'friday'],
|
||||
'12:30:00',
|
||||
5,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'15-01-2024 12:30:00',
|
||||
'19-01-2024 12:30:00',
|
||||
'22-01-2024 12:30:00',
|
||||
'26-01-2024 12:30:00',
|
||||
'29-01-2024 12:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate weekly occurrences including today if alert time is in the future', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'week',
|
||||
['monday', 'friday'],
|
||||
'10:30:00',
|
||||
5,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
// today included (15-01-2024 00:30:00)
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'15-01-2024 10:30:00',
|
||||
'19-01-2024 10:30:00',
|
||||
'22-01-2024 10:30:00',
|
||||
'26-01-2024 10:30:00',
|
||||
'29-01-2024 10:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate weekly occurrences excluding today if alert time is in the past', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'week',
|
||||
['monday', 'friday'],
|
||||
'00:00:00',
|
||||
5,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
// today excluded (15-01-2024 00:30:00)
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'19-01-2024 00:00:00',
|
||||
'22-01-2024 00:00:00',
|
||||
'26-01-2024 00:00:00',
|
||||
'29-01-2024 00:00:00',
|
||||
'02-02-2024 00:00:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate weekly occurrences excluding today if alert time is in the present (right now)', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'week',
|
||||
['monday', 'friday'],
|
||||
'00:30:00',
|
||||
5,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
// today excluded (15-01-2024 00:30:00)
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'19-01-2024 00:30:00',
|
||||
'22-01-2024 00:30:00',
|
||||
'26-01-2024 00:30:00',
|
||||
'29-01-2024 00:30:00',
|
||||
'02-02-2024 00:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate monthly occurrences including today if alert time is in the future', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'month',
|
||||
['15'],
|
||||
'10:30:00',
|
||||
5,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
// today included (15-01-2024 10:30:00)
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'15-01-2024 10:30:00',
|
||||
'15-02-2024 10:30:00',
|
||||
'15-03-2024 10:30:00',
|
||||
'15-04-2024 10:30:00',
|
||||
'15-05-2024 10:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate monthly occurrences excluding today if alert time is in the past', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'month',
|
||||
['15'],
|
||||
'00:00:00',
|
||||
5,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
// today excluded (15-01-2024 10:30:00)
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'15-02-2024 00:00:00',
|
||||
'15-03-2024 00:00:00',
|
||||
'15-04-2024 00:00:00',
|
||||
'15-05-2024 00:00:00',
|
||||
'15-06-2024 00:00:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate monthly occurrences excluding today if alert time is in the present (right now)', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'month',
|
||||
['15'],
|
||||
'00:30:00',
|
||||
5,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
// today excluded (15-01-2024 10:30:00)
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'15-02-2024 00:30:00',
|
||||
'15-03-2024 00:30:00',
|
||||
'15-04-2024 00:30:00',
|
||||
'15-05-2024 00:30:00',
|
||||
'15-06-2024 00:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should account for february 29th in a leap year', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'month',
|
||||
['29'],
|
||||
'10:30:00',
|
||||
5,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'29-01-2024 10:30:00',
|
||||
'29-02-2024 10:30:00',
|
||||
'29-03-2024 10:30:00',
|
||||
'29-04-2024 10:30:00',
|
||||
'29-05-2024 10:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip 31st on 30-day months', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'month',
|
||||
['31'],
|
||||
'10:30:00',
|
||||
5,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'31-01-2024 10:30:00',
|
||||
'31-03-2024 10:30:00',
|
||||
'31-05-2024 10:30:00',
|
||||
'31-07-2024 10:30:00',
|
||||
'31-08-2024 10:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should skip february 29th in a non-leap year', async () => {
|
||||
jest.resetModules(); // clear previous mocks
|
||||
|
||||
jest.doMock('dayjs', () => {
|
||||
const originalDayjs = jest.requireActual('dayjs');
|
||||
const mockDayjs = (date?: string | Date): Dayjs => {
|
||||
if (date) return originalDayjs(date);
|
||||
return originalDayjs(MOCK_DATE_STRING_NON_LEAP_YEAR);
|
||||
};
|
||||
Object.assign(mockDayjs, originalDayjs);
|
||||
return mockDayjs;
|
||||
});
|
||||
|
||||
const { buildAlertScheduleFromCustomSchedule } = await import('../utils');
|
||||
const { default: dayjs } = await import('dayjs');
|
||||
|
||||
const formatDate = (date: Date): string =>
|
||||
dayjs(date).format('DD-MM-YYYY HH:mm:ss');
|
||||
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'month',
|
||||
['29'],
|
||||
'10:30:00',
|
||||
5,
|
||||
);
|
||||
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'29-01-2023 10:30:00',
|
||||
'29-03-2023 10:30:00',
|
||||
'29-04-2023 10:30:00',
|
||||
'29-05-2023 10:30:00',
|
||||
'29-06-2023 10:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate daily occurrences', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'day',
|
||||
[],
|
||||
'10:40:00',
|
||||
5,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'15-01-2024 10:40:00',
|
||||
'16-01-2024 10:40:00',
|
||||
'17-01-2024 10:40:00',
|
||||
'18-01-2024 10:40:00',
|
||||
'19-01-2024 10:40:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate daily occurrences excluding today if alert time is in the past', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'day',
|
||||
[],
|
||||
'00:00:00',
|
||||
5,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'16-01-2024 00:00:00',
|
||||
'17-01-2024 00:00:00',
|
||||
'18-01-2024 00:00:00',
|
||||
'19-01-2024 00:00:00',
|
||||
'20-01-2024 00:00:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate daily occurrences excluding today if alert time is in the present (right now)', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'day',
|
||||
[],
|
||||
'00:30:00',
|
||||
5,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'16-01-2024 00:30:00',
|
||||
'17-01-2024 00:30:00',
|
||||
'18-01-2024 00:30:00',
|
||||
'19-01-2024 00:30:00',
|
||||
'20-01-2024 00:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate daily occurrences including today if alert time is in the future', () => {
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'day',
|
||||
[],
|
||||
'10:30:00',
|
||||
5,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'15-01-2024 10:30:00',
|
||||
'16-01-2024 10:30:00',
|
||||
'17-01-2024 10:30:00',
|
||||
'18-01-2024 10:30:00',
|
||||
'19-01-2024 10:30:00',
|
||||
]);
|
||||
});
|
||||
|
||||
it('daily occurrences should span across months correctly', async () => {
|
||||
jest.resetModules(); // clear previous mocks
|
||||
|
||||
jest.doMock('dayjs', () => {
|
||||
const originalDayjs = jest.requireActual('dayjs');
|
||||
const mockDayjs = (date?: string | Date): Dayjs => {
|
||||
if (date) return originalDayjs(date);
|
||||
return originalDayjs(MOCK_DATE_STRING_SPANS_MONTHS);
|
||||
};
|
||||
Object.assign(mockDayjs, originalDayjs);
|
||||
return mockDayjs;
|
||||
});
|
||||
|
||||
const { buildAlertScheduleFromCustomSchedule } = await import('../utils');
|
||||
const { default: dayjs } = await import('dayjs');
|
||||
|
||||
const formatDate = (date: Date): string =>
|
||||
dayjs(date).format('DD-MM-YYYY HH:mm:ss');
|
||||
|
||||
const result = buildAlertScheduleFromCustomSchedule(
|
||||
'day',
|
||||
[],
|
||||
'10:30:00',
|
||||
5,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result?.map((res) => formatDate(res))).toEqual([
|
||||
'31-01-2024 10:30:00',
|
||||
'01-02-2024 10:30:00',
|
||||
'02-02-2024 10:30:00',
|
||||
'03-02-2024 10:30:00',
|
||||
'04-02-2024 10:30:00',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidRRule', () => {
|
||||
beforeEach(() => {
|
||||
(rrulestr as jest.Mock).mockReturnValue({});
|
||||
});
|
||||
|
||||
it('should return true for valid rrule', () => {
|
||||
expect(isValidRRule(FREQ_DAILY)).toBe(true);
|
||||
expect(rrulestr).toHaveBeenCalledWith(FREQ_DAILY);
|
||||
});
|
||||
|
||||
it('should handle escaped newlines', () => {
|
||||
expect(isValidRRule('FREQ=DAILY\\nINTERVAL=1')).toBe(true);
|
||||
expect(rrulestr).toHaveBeenCalledWith('FREQ=DAILY\nINTERVAL=1');
|
||||
});
|
||||
|
||||
it('should return false for invalid rrule', () => {
|
||||
(rrulestr as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Invalid rrule');
|
||||
});
|
||||
|
||||
expect(isValidRRule('INVALID')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { generateTimezoneData } from 'components/CustomTimePicker/timezoneUtils';
|
||||
|
||||
export const EVALUATION_WINDOW_TYPE = [
|
||||
{ label: 'Rolling', value: 'rolling' },
|
||||
{ label: 'Cumulative', value: 'cumulative' },
|
||||
];
|
||||
|
||||
export const EVALUATION_WINDOW_TIMEFRAME = {
|
||||
rolling: [
|
||||
{ label: 'Last 5 minutes', value: '5m0s' },
|
||||
{ label: 'Last 10 minutes', value: '10m0s' },
|
||||
{ label: 'Last 15 minutes', value: '15m0s' },
|
||||
{ label: 'Last 30 minutes', value: '30m0s' },
|
||||
{ label: 'Last 1 hour', value: '1h0m0s' },
|
||||
{ label: 'Last 2 hours', value: '2h0m0s' },
|
||||
{ label: 'Last 4 hours', value: '4h0m0s' },
|
||||
],
|
||||
cumulative: [
|
||||
{ label: 'Current hour', value: 'currentHour' },
|
||||
{ label: 'Current day', value: 'currentDay' },
|
||||
{ label: 'Current month', value: 'currentMonth' },
|
||||
],
|
||||
};
|
||||
|
||||
export const EVALUATION_CADENCE_REPEAT_EVERY_OPTIONS = [
|
||||
{ label: 'WEEK', value: 'week' },
|
||||
{ label: 'MONTH', value: 'month' },
|
||||
];
|
||||
|
||||
export const EVALUATION_CADENCE_REPEAT_EVERY_WEEK_OPTIONS = [
|
||||
{ label: 'SUNDAY', value: 'sunday' },
|
||||
{ label: 'MONDAY', value: 'monday' },
|
||||
{ label: 'TUESDAY', value: 'tuesday' },
|
||||
{ label: 'WEDNESDAY', value: 'wednesday' },
|
||||
{ label: 'THURSDAY', value: 'thursday' },
|
||||
{ label: 'FRIDAY', value: 'friday' },
|
||||
{ label: 'SATURDAY', value: 'saturday' },
|
||||
];
|
||||
|
||||
export const EVALUATION_CADENCE_REPEAT_EVERY_MONTH_OPTIONS = Array.from(
|
||||
{ length: 31 },
|
||||
(_, i) => {
|
||||
const value = String(i + 1);
|
||||
return { label: value, value };
|
||||
},
|
||||
);
|
||||
|
||||
export const WEEKDAY_MAP: { [key: string]: number } = {
|
||||
sunday: 0,
|
||||
monday: 1,
|
||||
tuesday: 2,
|
||||
wednesday: 3,
|
||||
thursday: 4,
|
||||
friday: 5,
|
||||
saturday: 6,
|
||||
};
|
||||
|
||||
export const TIMEZONE_DATA = generateTimezoneData().map((timezone) => ({
|
||||
label: `${timezone.name} (${timezone.offset})`,
|
||||
value: timezone.value,
|
||||
}));
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import {
|
||||
EvaluationWindowAction,
|
||||
EvaluationWindowState,
|
||||
} from '../context/types';
|
||||
|
||||
export interface IAdvancedOptionItemProps {
|
||||
title: string;
|
||||
description: string;
|
||||
input: JSX.Element;
|
||||
}
|
||||
|
||||
export enum RollingWindowTimeframes {
|
||||
'LAST_5_MINUTES' = '5m0s',
|
||||
'LAST_10_MINUTES' = '10m0s',
|
||||
'LAST_15_MINUTES' = '15m0s',
|
||||
'LAST_30_MINUTES' = '30m0s',
|
||||
'LAST_1_HOUR' = '1h0m0s',
|
||||
'LAST_2_HOURS' = '2h0m0s',
|
||||
'LAST_4_HOURS' = '4h0m0s',
|
||||
}
|
||||
|
||||
export enum CumulativeWindowTimeframes {
|
||||
'CURRENT_HOUR' = 'currentHour',
|
||||
'CURRENT_DAY' = 'currentDay',
|
||||
'CURRENT_MONTH' = 'currentMonth',
|
||||
}
|
||||
|
||||
export interface IEvaluationWindowPopoverProps {
|
||||
evaluationWindow: EvaluationWindowState;
|
||||
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export interface IEvaluationWindowDetailsProps {
|
||||
evaluationWindow: EvaluationWindowState;
|
||||
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
||||
}
|
||||
|
||||
export interface IEvaluationCadenceDetailsProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export interface TimeInputProps {
|
||||
value?: string; // Format: "HH:MM:SS"
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { rrulestr } from 'rrule';
|
||||
|
||||
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../context/constants';
|
||||
import { EvaluationWindowState } from '../context/types';
|
||||
import { WEEKDAY_MAP } from './constants';
|
||||
import { CumulativeWindowTimeframes, RollingWindowTimeframes } from './types';
|
||||
|
||||
// Extend dayjs with timezone plugins
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export const getEvaluationWindowTypeText = (
|
||||
windowType: 'rolling' | 'cumulative',
|
||||
): string => {
|
||||
switch (windowType) {
|
||||
case 'rolling':
|
||||
return 'Rolling';
|
||||
case 'cumulative':
|
||||
return 'Cumulative';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const getCumulativeWindowTimeframeText = (
|
||||
evaluationWindow: EvaluationWindowState,
|
||||
): string => {
|
||||
switch (evaluationWindow.timeframe) {
|
||||
case CumulativeWindowTimeframes.CURRENT_HOUR:
|
||||
return `Current hour, starting at minute ${evaluationWindow.startingAt.number} (${evaluationWindow.startingAt.timezone})`;
|
||||
case CumulativeWindowTimeframes.CURRENT_DAY:
|
||||
return `Current day, starting from ${evaluationWindow.startingAt.time} (${evaluationWindow.startingAt.timezone})`;
|
||||
case CumulativeWindowTimeframes.CURRENT_MONTH:
|
||||
return `Current month, starting from day ${evaluationWindow.startingAt.number} at ${evaluationWindow.startingAt.time} (${evaluationWindow.startingAt.timezone})`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const getRollingWindowTimeframeText = (
|
||||
timeframe: RollingWindowTimeframes,
|
||||
): string => {
|
||||
switch (timeframe) {
|
||||
case RollingWindowTimeframes.LAST_5_MINUTES:
|
||||
return 'Last 5 minutes';
|
||||
case RollingWindowTimeframes.LAST_10_MINUTES:
|
||||
return 'Last 10 minutes';
|
||||
case RollingWindowTimeframes.LAST_15_MINUTES:
|
||||
return 'Last 15 minutes';
|
||||
case RollingWindowTimeframes.LAST_30_MINUTES:
|
||||
return 'Last 30 minutes';
|
||||
case RollingWindowTimeframes.LAST_1_HOUR:
|
||||
return 'Last 1 hour';
|
||||
case RollingWindowTimeframes.LAST_2_HOURS:
|
||||
return 'Last 2 hours';
|
||||
case RollingWindowTimeframes.LAST_4_HOURS:
|
||||
return 'Last 4 hours';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const getCustomRollingWindowTimeframeText = (
|
||||
evaluationWindow: EvaluationWindowState,
|
||||
): string =>
|
||||
`Last ${evaluationWindow.startingAt.number} ${
|
||||
ADVANCED_OPTIONS_TIME_UNIT_OPTIONS.find(
|
||||
(option) => option.value === evaluationWindow.startingAt.unit,
|
||||
)?.label
|
||||
}`;
|
||||
|
||||
export const getTimeframeText = (
|
||||
evaluationWindow: EvaluationWindowState,
|
||||
): string => {
|
||||
if (evaluationWindow.windowType === 'rolling') {
|
||||
if (evaluationWindow.timeframe === 'custom') {
|
||||
return getCustomRollingWindowTimeframeText(evaluationWindow);
|
||||
}
|
||||
return getRollingWindowTimeframeText(
|
||||
evaluationWindow.timeframe as RollingWindowTimeframes,
|
||||
);
|
||||
}
|
||||
return getCumulativeWindowTimeframeText(evaluationWindow);
|
||||
};
|
||||
|
||||
export function buildAlertScheduleFromRRule(
|
||||
rruleString: string,
|
||||
date: Dayjs | null,
|
||||
startAt: string,
|
||||
maxOccurrences = 10,
|
||||
): Date[] | null {
|
||||
try {
|
||||
if (!rruleString) return null;
|
||||
|
||||
// Handle literal \n in string
|
||||
let finalRRuleString = rruleString.replace(/\\n/g, '\n');
|
||||
|
||||
if (date) {
|
||||
const dt = dayjs(date);
|
||||
if (!dt.isValid()) throw new Error('Invalid date provided');
|
||||
|
||||
const [hours = 0, minutes = 0, seconds = 0] = startAt.split(':').map(Number);
|
||||
|
||||
const dtWithTime = dt
|
||||
.set('hour', hours)
|
||||
.set('minute', minutes)
|
||||
.set('second', seconds)
|
||||
.set('millisecond', 0);
|
||||
|
||||
const dtStartStr = dtWithTime
|
||||
.toISOString()
|
||||
.replace(/[-:]/g, '')
|
||||
.replace(/\.\d{3}Z$/, 'Z');
|
||||
|
||||
if (!/DTSTART/i.test(finalRRuleString)) {
|
||||
finalRRuleString = `DTSTART:${dtStartStr}\n${finalRRuleString}`;
|
||||
}
|
||||
}
|
||||
|
||||
const rruleObj = rrulestr(finalRRuleString);
|
||||
const occurrences: Date[] = [];
|
||||
rruleObj.all((date, index) => {
|
||||
if (index >= maxOccurrences) return false;
|
||||
occurrences.push(date);
|
||||
return true;
|
||||
});
|
||||
|
||||
return occurrences;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function generateMonthlyOccurrences(
|
||||
targetDays: number[],
|
||||
hours: number,
|
||||
minutes: number,
|
||||
seconds: number,
|
||||
maxOccurrences: number,
|
||||
): Date[] {
|
||||
const occurrences: Date[] = [];
|
||||
const currentMonth = dayjs().startOf('month');
|
||||
|
||||
const currentDate = dayjs();
|
||||
|
||||
const scanMonths = maxOccurrences + 12;
|
||||
for (let monthOffset = 0; monthOffset < scanMonths; monthOffset++) {
|
||||
const monthDate = currentMonth.add(monthOffset, 'month');
|
||||
targetDays.forEach((day) => {
|
||||
if (occurrences.length >= maxOccurrences) return;
|
||||
|
||||
const daysInMonth = monthDate.daysInMonth();
|
||||
if (day <= daysInMonth) {
|
||||
const targetDate = monthDate
|
||||
.date(day)
|
||||
.hour(hours)
|
||||
.minute(minutes)
|
||||
.second(seconds);
|
||||
if (targetDate.isAfter(currentDate)) {
|
||||
occurrences.push(targetDate.toDate());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
function generateWeeklyOccurrences(
|
||||
targetWeekdays: number[],
|
||||
hours: number,
|
||||
minutes: number,
|
||||
seconds: number,
|
||||
maxOccurrences: number,
|
||||
): Date[] {
|
||||
const occurrences: Date[] = [];
|
||||
const currentWeek = dayjs().startOf('week');
|
||||
|
||||
const currentDate = dayjs();
|
||||
|
||||
for (let weekOffset = 0; weekOffset < maxOccurrences; weekOffset++) {
|
||||
const weekDate = currentWeek.add(weekOffset, 'week');
|
||||
targetWeekdays.forEach((weekday) => {
|
||||
if (occurrences.length >= maxOccurrences) return;
|
||||
|
||||
const targetDate = weekDate
|
||||
.day(weekday)
|
||||
.hour(hours)
|
||||
.minute(minutes)
|
||||
.second(seconds);
|
||||
if (targetDate.isAfter(currentDate)) {
|
||||
occurrences.push(targetDate.toDate());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
export function generateDailyOccurrences(
|
||||
hours: number,
|
||||
minutes: number,
|
||||
seconds: number,
|
||||
maxOccurrences: number,
|
||||
): Date[] {
|
||||
const occurrences: Date[] = [];
|
||||
const currentDate = dayjs();
|
||||
const currentTime =
|
||||
currentDate.hour() * 3600 + currentDate.minute() * 60 + currentDate.second();
|
||||
const targetTime = hours * 3600 + minutes * 60 + seconds;
|
||||
|
||||
// Start from today if target time is after current time, otherwise start from tomorrow
|
||||
const startDayOffset = targetTime > currentTime ? 0 : 1;
|
||||
|
||||
for (
|
||||
let dayOffset = startDayOffset;
|
||||
dayOffset < startDayOffset + maxOccurrences;
|
||||
dayOffset++
|
||||
) {
|
||||
const dayDate = currentDate.add(dayOffset, 'day');
|
||||
const targetDate = dayDate.hour(hours).minute(minutes).second(seconds);
|
||||
occurrences.push(targetDate.toDate());
|
||||
}
|
||||
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
export function buildAlertScheduleFromCustomSchedule(
|
||||
repeatEvery: string,
|
||||
occurence: string[],
|
||||
startAt: string,
|
||||
maxOccurrences = 10,
|
||||
): Date[] | null {
|
||||
try {
|
||||
const [hours = 0, minutes = 0, seconds = 0] = startAt.split(':').map(Number);
|
||||
let occurrences: Date[] = [];
|
||||
|
||||
if (repeatEvery === 'month') {
|
||||
const targetDays = occurence
|
||||
.map((day) => parseInt(day, 10))
|
||||
.filter((day) => !Number.isNaN(day));
|
||||
occurrences = generateMonthlyOccurrences(
|
||||
targetDays,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
maxOccurrences,
|
||||
);
|
||||
} else if (repeatEvery === 'week') {
|
||||
const targetWeekdays = occurence
|
||||
.map((day) => WEEKDAY_MAP[day.toLowerCase()])
|
||||
.filter((day) => day !== undefined);
|
||||
occurrences = generateWeeklyOccurrences(
|
||||
targetWeekdays,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
maxOccurrences,
|
||||
);
|
||||
} else if (repeatEvery === 'day') {
|
||||
occurrences = generateDailyOccurrences(
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
maxOccurrences,
|
||||
);
|
||||
}
|
||||
|
||||
occurrences.sort((a, b) => a.getTime() - b.getTime());
|
||||
return occurrences.slice(0, maxOccurrences);
|
||||
} catch (error) {
|
||||
Sentry.captureEvent({
|
||||
message: `Error building alert schedule from custom schedule: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
level: 'error',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidRRule(rruleString: string): boolean {
|
||||
try {
|
||||
// normalize escaped \n
|
||||
const finalRRuleString = rruleString.replace(/\\n/g, '\n');
|
||||
rrulestr(finalRRuleString); // will throw if invalid
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { TIMEZONE_DATA } from 'container/CreateAlertV2/EvaluationSettings/constants';
|
||||
import dayjs from 'dayjs';
|
||||
import getRandomColor from 'lib/getRandomColor';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
AlertState,
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
AlertThresholdState,
|
||||
Algorithm,
|
||||
EvaluationWindowState,
|
||||
Seasonality,
|
||||
Threshold,
|
||||
TimeDuration,
|
||||
@@ -70,6 +75,49 @@ export const INITIAL_ALERT_THRESHOLD_STATE: AlertThresholdState = {
|
||||
thresholds: [INITIAL_CRITICAL_THRESHOLD],
|
||||
};
|
||||
|
||||
export const INITIAL_ADVANCED_OPTIONS_STATE: AdvancedOptionsState = {
|
||||
sendNotificationIfDataIsMissing: {
|
||||
toleranceLimit: 15,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
enforceMinimumDatapoints: {
|
||||
minimumDatapoints: 0,
|
||||
},
|
||||
delayEvaluation: {
|
||||
delay: 5,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
evaluationCadence: {
|
||||
mode: 'default',
|
||||
default: {
|
||||
value: 1,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
custom: {
|
||||
repeatEvery: 'week',
|
||||
startAt: '00:00:00',
|
||||
occurence: [],
|
||||
timezone: TIMEZONE_DATA[0].value,
|
||||
},
|
||||
rrule: {
|
||||
date: dayjs(),
|
||||
startAt: '00:00:00',
|
||||
rrule: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const INITIAL_EVALUATION_WINDOW_STATE: EvaluationWindowState = {
|
||||
windowType: 'rolling',
|
||||
timeframe: '5m0s',
|
||||
startingAt: {
|
||||
time: '00:00:00',
|
||||
number: '1',
|
||||
timezone: TIMEZONE_DATA[0].value,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
};
|
||||
|
||||
export const THRESHOLD_OPERATOR_OPTIONS = [
|
||||
{ value: AlertThresholdOperator.IS_ABOVE, label: 'IS ABOVE' },
|
||||
{ value: AlertThresholdOperator.IS_BELOW, label: 'IS BELOW' },
|
||||
@@ -115,3 +163,10 @@ export const ANOMALY_SEASONALITY_OPTIONS = [
|
||||
{ value: Seasonality.DAILY, label: 'Daily' },
|
||||
{ value: Seasonality.WEEKLY, label: 'Weekly' },
|
||||
];
|
||||
|
||||
export const ADVANCED_OPTIONS_TIME_UNIT_OPTIONS = [
|
||||
{ value: UniversalYAxisUnit.SECONDS, label: 'Seconds' },
|
||||
{ value: UniversalYAxisUnit.MINUTES, label: 'Minutes' },
|
||||
{ value: UniversalYAxisUnit.HOURS, label: 'Hours' },
|
||||
{ value: UniversalYAxisUnit.DAYS, label: 'Days' },
|
||||
];
|
||||
|
||||
@@ -14,14 +14,18 @@ import { useLocation } from 'react-router-dom';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import {
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
INITIAL_ALERT_STATE,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
} from './constants';
|
||||
import { ICreateAlertContextProps, ICreateAlertProviderProps } from './types';
|
||||
import {
|
||||
advancedOptionsReducer,
|
||||
alertCreationReducer,
|
||||
alertThresholdReducer,
|
||||
buildInitialAlertDef,
|
||||
evaluationWindowReducer,
|
||||
getInitialAlertTypeFromURL,
|
||||
} from './utils';
|
||||
|
||||
@@ -80,6 +84,16 @@ export function CreateAlertProvider(
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
);
|
||||
|
||||
const [evaluationWindow, setEvaluationWindow] = useReducer(
|
||||
evaluationWindowReducer,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
);
|
||||
|
||||
const [advancedOptions, setAdvancedOptions] = useReducer(
|
||||
advancedOptionsReducer,
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setThresholdState({
|
||||
type: 'RESET',
|
||||
@@ -94,8 +108,19 @@ export function CreateAlertProvider(
|
||||
setAlertType: handleAlertTypeChange,
|
||||
thresholdState,
|
||||
setThresholdState,
|
||||
evaluationWindow,
|
||||
setEvaluationWindow,
|
||||
advancedOptions,
|
||||
setAdvancedOptions,
|
||||
}),
|
||||
[alertState, alertType, handleAlertTypeChange, thresholdState],
|
||||
[
|
||||
alertState,
|
||||
alertType,
|
||||
handleAlertTypeChange,
|
||||
thresholdState,
|
||||
evaluationWindow,
|
||||
advancedOptions,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { Dispatch } from 'react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
@@ -9,6 +10,10 @@ export interface ICreateAlertContextProps {
|
||||
setAlertType: Dispatch<AlertTypes>;
|
||||
thresholdState: AlertThresholdState;
|
||||
setThresholdState: Dispatch<AlertThresholdAction>;
|
||||
advancedOptions: AdvancedOptionsState;
|
||||
setAdvancedOptions: Dispatch<AdvancedOptionsAction>;
|
||||
evaluationWindow: EvaluationWindowState;
|
||||
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
||||
}
|
||||
|
||||
export interface ICreateAlertProviderProps {
|
||||
@@ -101,3 +106,87 @@ export type AlertThresholdAction =
|
||||
| { type: 'SET_SEASONALITY'; payload: string }
|
||||
| { type: 'SET_THRESHOLDS'; payload: Threshold[] }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export interface AdvancedOptionsState {
|
||||
sendNotificationIfDataIsMissing: {
|
||||
toleranceLimit: number;
|
||||
timeUnit: string;
|
||||
};
|
||||
enforceMinimumDatapoints: {
|
||||
minimumDatapoints: number;
|
||||
};
|
||||
delayEvaluation: {
|
||||
delay: number;
|
||||
timeUnit: string;
|
||||
};
|
||||
evaluationCadence: {
|
||||
mode: EvaluationCadenceMode;
|
||||
default: {
|
||||
value: number;
|
||||
timeUnit: string;
|
||||
};
|
||||
custom: {
|
||||
repeatEvery: string;
|
||||
startAt: string;
|
||||
occurence: string[];
|
||||
timezone: string;
|
||||
};
|
||||
rrule: {
|
||||
date: Dayjs | null;
|
||||
startAt: string;
|
||||
rrule: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type AdvancedOptionsAction =
|
||||
| {
|
||||
type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING';
|
||||
payload: { toleranceLimit: number; timeUnit: string };
|
||||
}
|
||||
| {
|
||||
type: 'SET_ENFORCE_MINIMUM_DATAPOINTS';
|
||||
payload: { minimumDatapoints: number };
|
||||
}
|
||||
| {
|
||||
type: 'SET_DELAY_EVALUATION';
|
||||
payload: { delay: number; timeUnit: string };
|
||||
}
|
||||
| {
|
||||
type: 'SET_EVALUATION_CADENCE';
|
||||
payload: {
|
||||
default: { value: number; timeUnit: string };
|
||||
custom: {
|
||||
repeatEvery: string;
|
||||
startAt: string;
|
||||
timezone: string;
|
||||
occurence: string[];
|
||||
};
|
||||
rrule: { date: Dayjs | null; startAt: string; rrule: string };
|
||||
};
|
||||
}
|
||||
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export interface EvaluationWindowState {
|
||||
windowType: 'rolling' | 'cumulative';
|
||||
timeframe: string;
|
||||
startingAt: {
|
||||
time: string;
|
||||
number: string;
|
||||
timezone: string;
|
||||
unit: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type EvaluationWindowAction =
|
||||
| { type: 'SET_WINDOW_TYPE'; payload: 'rolling' | 'cumulative' }
|
||||
| { type: 'SET_TIMEFRAME'; payload: string }
|
||||
| {
|
||||
type: 'SET_STARTING_AT';
|
||||
payload: { time: string; number: string; timezone: string; unit: string };
|
||||
}
|
||||
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export type EvaluationCadenceMode = 'default' | 'custom' | 'rrule';
|
||||
|
||||
@@ -11,12 +11,20 @@ import { AlertDef } from 'types/api/alerts/def';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { INITIAL_ALERT_THRESHOLD_STATE } from './constants';
|
||||
import {
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
} from './constants';
|
||||
import {
|
||||
AdvancedOptionsAction,
|
||||
AdvancedOptionsState,
|
||||
AlertState,
|
||||
AlertThresholdAction,
|
||||
AlertThresholdState,
|
||||
CreateAlertAction,
|
||||
EvaluationWindowAction,
|
||||
EvaluationWindowState,
|
||||
} from './types';
|
||||
|
||||
export const alertCreationReducer = (
|
||||
@@ -110,3 +118,57 @@ export const alertThresholdReducer = (
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const advancedOptionsReducer = (
|
||||
state: AdvancedOptionsState,
|
||||
action: AdvancedOptionsAction,
|
||||
): AdvancedOptionsState => {
|
||||
switch (action.type) {
|
||||
case 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING':
|
||||
return { ...state, sendNotificationIfDataIsMissing: action.payload };
|
||||
case 'SET_ENFORCE_MINIMUM_DATAPOINTS':
|
||||
return { ...state, enforceMinimumDatapoints: action.payload };
|
||||
case 'SET_DELAY_EVALUATION':
|
||||
return { ...state, delayEvaluation: action.payload };
|
||||
case 'SET_EVALUATION_CADENCE':
|
||||
return {
|
||||
...state,
|
||||
evaluationCadence: { ...state.evaluationCadence, ...action.payload },
|
||||
};
|
||||
case 'SET_EVALUATION_CADENCE_MODE':
|
||||
return {
|
||||
...state,
|
||||
evaluationCadence: { ...state.evaluationCadence, mode: action.payload },
|
||||
};
|
||||
case 'RESET':
|
||||
return INITIAL_ADVANCED_OPTIONS_STATE;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const evaluationWindowReducer = (
|
||||
state: EvaluationWindowState,
|
||||
action: EvaluationWindowAction,
|
||||
): EvaluationWindowState => {
|
||||
switch (action.type) {
|
||||
case 'SET_WINDOW_TYPE':
|
||||
return {
|
||||
...state,
|
||||
windowType: action.payload,
|
||||
startingAt: INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
timeframe:
|
||||
action.payload === 'rolling'
|
||||
? INITIAL_EVALUATION_WINDOW_STATE.timeframe
|
||||
: 'currentHour',
|
||||
};
|
||||
case 'SET_TIMEFRAME':
|
||||
return { ...state, timeframe: action.payload };
|
||||
case 'SET_STARTING_AT':
|
||||
return { ...state, startingAt: action.payload };
|
||||
case 'RESET':
|
||||
return INITIAL_EVALUATION_WINDOW_STATE;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16122,6 +16122,13 @@ robust-predicates@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
|
||||
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
|
||||
|
||||
rrule@2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.8.1.tgz#e8341a9ce3e68ce5b8da4d502e893cd9f286805e"
|
||||
integrity sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
rtl-css-js@^1.14.0, rtl-css-js@^1.16.1:
|
||||
version "1.16.1"
|
||||
resolved "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz"
|
||||
|
||||
Reference in New Issue
Block a user