diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx b/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx index e45a44d2d2b4..a0dd357cd01c 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx +++ b/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx @@ -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 { {alertType !== AlertTypes.ANOMALY_BASED_ALERT && } {alertType === AlertTypes.ANOMALY_BASED_ALERT && } + {showCondensedLayoutFlag ? ( +
+ +
+ ) : null} ); } diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx b/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx index 1610602daf2f..ca1934a774a6 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx +++ b/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx @@ -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 ? ( + + ) : ( + Evaluation Window. + ); + return ( -
+
{/* Main condition sentence */}
@@ -128,7 +142,7 @@ function AlertThreshold(): JSX.Element { options={THRESHOLD_MATCH_TYPE_OPTIONS} /> - during the Evaluation Window. + during the {evaluationWindowContext}
diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss b/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss index bdde72598d6a..8e9fc6e223cc 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss +++ b/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss @@ -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; + } + } + } +} diff --git a/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx b/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx index 589a18fc6ca1..185241254d3b 100644 --- a/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx +++ b/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx @@ -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 ( -
- + +
- -
+ {!showCondensedLayoutFlag ? : null} +
+ ); } diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx b/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx new file mode 100644 index 000000000000..a560c449e8aa --- /dev/null +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx @@ -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 ( +
+ + + + + + setAdvancedOptions({ + type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING', + payload: { + toleranceLimit: Number(e.target.value), + timeUnit: advancedOptions.sendNotificationIfDataIsMissing.timeUnit, + }, + }) + } + value={advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit} + /> + + setAdvancedOptions({ + type: 'SET_ENFORCE_MINIMUM_DATAPOINTS', + payload: { + minimumDatapoints: Number(e.target.value), + }, + }) + } + value={advancedOptions.enforceMinimumDatapoints.minimumDatapoints} + /> + Datapoints +
+ } + /> + + + setAdvancedOptions({ + type: 'SET_DELAY_EVALUATION', + payload: { + delay: Number(e.target.value), + timeUnit: advancedOptions.delayEvaluation.timeUnit, + }, + }) + } + value={advancedOptions.delayEvaluation.delay} + /> + +
+
+ ); + } + + if (isCurrentDay) { + return ( +
+ {CUMULATIVE_WINDOW_DESCRIPTION} + {displayText} +
+ STARTING AT + +
+
+ SELECT TIMEZONE + +
+
+ STARTING AT + +
+
+ SELECT TIMEZONE + handleNumberChange(e.target.value)} + placeholder="Enter value" + /> +
+
+ UNIT + +
+ ), +})); + +describe('EvaluationWindowPopover', () => { + it('should render the evaluation window popover with 3 sections', () => { + render( + , + ); + expect(screen.getByText(EVALUATION_WINDOW_TEXT)).toBeInTheDocument(); + }); + + it('should render all window type options with rolling selected', () => { + render( + , + ); + 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( + , + ); + 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( + , + ); + 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( + , + ); + 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( + , + ); + 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( + , + ); + + 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( + , + ); + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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', + }); + }); + }); +}); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts index f73910bec1f2..b1f9029ef440 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts @@ -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 => ({ + ...INITIAL_EVALUATION_WINDOW_STATE, + ...overrides, +}); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/constants.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/constants.ts index 61f07dc8829b..4319b765565e 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/constants.ts +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/constants.ts @@ -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.'; diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/index.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/index.ts new file mode 100644 index 000000000000..e3637027affc --- /dev/null +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/index.ts @@ -0,0 +1,3 @@ +import EvaluationSettings from './EvaluationSettings'; + +export default EvaluationSettings; diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/styles.scss b/frontend/src/container/CreateAlertV2/EvaluationSettings/styles.scss index 80860e4ead46..6cb15a2a87ef 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/styles.scss +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/styles.scss @@ -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); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts index 5b6fecc4e97f..f3f0afadd69c 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts @@ -32,8 +32,6 @@ export enum CumulativeWindowTimeframes { export interface IEvaluationWindowPopoverProps { evaluationWindow: EvaluationWindowState; setEvaluationWindow: Dispatch; - isOpen: boolean; - setIsOpen: Dispatch>; } export interface IEvaluationWindowDetailsProps { diff --git a/frontend/src/container/CreateAlertV2/utils.tsx b/frontend/src/container/CreateAlertV2/utils.tsx index 9451a2fc55f7..f164e9bed149 100644 --- a/frontend/src/container/CreateAlertV2/utils.tsx +++ b/frontend/src/container/CreateAlertV2/utils.tsx @@ -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';