Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
add1604
[Alerting V2] [UI] Add activation configuration fields to alerting V2…
yiannisnikolopoulos Feb 26, 2026
43f41d1
Merge branch 'alerting_v2' into feature/esql-rule-activation-configur…
yiannisnikolopoulos Feb 26, 2026
fa6bc78
Update condition handling in useCreateRule to use undefined instead o…
yiannisnikolopoulos Feb 26, 2026
79483f0
Update tests for ActivationConfigurationFieldGroup to include new act…
yiannisnikolopoulos Feb 26, 2026
8447ea7
Merge branch 'alerting_v2' into feature/esql-rule-activation-configur…
yiannisnikolopoulos Mar 2, 2026
a68c743
Merge branch 'alerting_v2' into feature/esql-rule-activation-configur…
yiannisnikolopoulos Mar 2, 2026
9ddd90e
Refactor alerting rule form to replace Activation Configuration with …
yiannisnikolopoulos Mar 3, 2026
fd283ad
Cleanup and refactor
yiannisnikolopoulos Mar 4, 2026
6d6d191
Update state transition components and form UI to match closer to the…
yiannisnikolopoulos Mar 4, 2026
0650773
Merge branch 'alerting_v2' into feature/esql-rule-activation-configur…
yiannisnikolopoulos Mar 4, 2026
d505909
Enforce max value of 1000 for state transition count
yiannisnikolopoulos Mar 4, 2026
7cb7adb
Enhance state transition form fields with default values and UI adjus…
yiannisnikolopoulos Mar 4, 2026
b4e5d3a
Merge branch 'alerting_v2' into feature/esql-rule-activation-configur…
yiannisnikolopoulos Mar 4, 2026
949216a
Undo compressed prop addition in rule form fields
yiannisnikolopoulos Mar 4, 2026
ed889c3
Refactor state transition form fields to use default values and simpl…
yiannisnikolopoulos Mar 4, 2026
d5f9ac0
Merge branch 'alerting_v2' into feature/esql-rule-activation-configur…
yiannisnikolopoulos Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const RuleFormFlyout: React.FC<RuleFormFlyoutProps> = ({
type={push ? 'push' : 'overlay'}
onClose={onClose || (() => {})}
aria-labelledby={FLYOUT_TITLE_ID}
size="s"
size="560px"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a fixed width because size "m" was too wide and size "s" didn't fit the fields properly. I chose 560px because that was the width used in the figma flyout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I have some changes coming to this in my PR. We can use this for now.

>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m" id={FLYOUT_TITLE_ID}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

export { RuleDetailsFieldGroup } from './rule_details_field_group';
export { RuleExecutionFieldGroup } from './rule_execution_field_group';
export { StateTransitionFieldGroup } from './state_transition_field_group';
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { StateTransitionFieldGroup } from './state_transition_field_group';
import { createFormWrapper } from '../../test_utils';

describe('StateTransitionFieldGroup', () => {
it('renders immediate mode by default when kind is "alert"', () => {
render(<StateTransitionFieldGroup />, {
wrapper: createFormWrapper({ kind: 'alert' }),
});

expect(screen.getByText('Alert delay')).toBeInTheDocument();
expect(screen.getByText('Immediate')).toBeInTheDocument();
expect(screen.getByText('Breaches')).toBeInTheDocument();
expect(screen.getByText('Duration')).toBeInTheDocument();
expect(screen.getByTestId('stateTransitionImmediateDescription')).toBeInTheDocument();
expect(screen.queryByTestId('stateTransitionCountInput')).not.toBeInTheDocument();
expect(screen.queryByTestId('stateTransitionTimeframeNumberInput')).not.toBeInTheDocument();
});

it('shows breaches input when breaches is selected', () => {
render(<StateTransitionFieldGroup />, {
wrapper: createFormWrapper({ kind: 'alert' }),
});

fireEvent.click(screen.getByRole('button', { name: 'Breaches' }));

expect(screen.getByTestId('stateTransitionCountInput')).toBeInTheDocument();
expect(screen.getByTestId('stateTransitionCountInput')).toHaveValue(2);
expect(screen.queryByTestId('stateTransitionImmediateDescription')).not.toBeInTheDocument();
expect(screen.queryByTestId('stateTransitionTimeframeNumberInput')).not.toBeInTheDocument();
});

it('does not render when kind is "signal"', () => {
render(<StateTransitionFieldGroup />, {
wrapper: createFormWrapper({ kind: 'signal' }),
});

expect(screen.queryByText('Alert delay')).not.toBeInTheDocument();
});

it('shows immediate mode text when immediate is selected', () => {
render(<StateTransitionFieldGroup />, {
wrapper: createFormWrapper({ kind: 'alert' }),
});

fireEvent.click(screen.getByRole('button', { name: 'Immediate' }));

expect(screen.getByTestId('stateTransitionImmediateDescription')).toBeInTheDocument();
expect(screen.queryByTestId('stateTransitionCountInput')).not.toBeInTheDocument();
expect(screen.queryByTestId('stateTransitionTimeframeNumberInput')).not.toBeInTheDocument();
});

it('shows duration inputs when duration is selected', () => {
render(<StateTransitionFieldGroup />, {
wrapper: createFormWrapper({ kind: 'alert' }),
});

fireEvent.click(screen.getByRole('button', { name: 'Duration' }));

expect(screen.getByTestId('stateTransitionTimeframeNumberInput')).toBeInTheDocument();
expect(screen.getByTestId('stateTransitionTimeframeUnitInput')).toBeInTheDocument();
expect(screen.getByTestId('stateTransitionTimeframeNumberInput')).toHaveValue(2);
expect(screen.getByTestId('stateTransitionTimeframeUnitInput')).toHaveValue('m');
expect(screen.queryByTestId('stateTransitionCountInput')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useCallback, useState } from 'react';
import { EuiButtonGroup, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useFormContext, useWatch } from 'react-hook-form';
import type { FormValues } from '../types';
import { FieldGroup } from './field_group';
import { StateTransitionCountField } from '../fields/state_transition_count_field';
import { StateTransitionTimeframeField } from '../fields/state_transition_timeframe_field';

type DelayMode = 'immediate' | 'breaches' | 'duration';

const MODE_OPTIONS = [
{
id: 'immediate' as const,
label: i18n.translate('xpack.alertingV2.ruleForm.stateTransition.delayModeImmediate', {
defaultMessage: 'Immediate',
}),
},
{
id: 'breaches' as const,
label: i18n.translate('xpack.alertingV2.ruleForm.stateTransition.delayModeBreaches', {
defaultMessage: 'Breaches',
}),
},
{
id: 'duration' as const,
label: i18n.translate('xpack.alertingV2.ruleForm.stateTransition.delayModeDuration', {
defaultMessage: 'Duration',
}),
},
];

const DEFAULT_PENDING_COUNT = 2;
const DEFAULT_PENDING_TIMEFRAME = '2m';

const deriveMode = (stateTransition?: {
pendingTimeframe?: string;
pendingCount?: number;
}): DelayMode => {
if (stateTransition?.pendingTimeframe != null) return 'duration';
if (stateTransition?.pendingCount != null) return 'breaches';
return 'immediate';
};

export const StateTransitionFieldGroup: React.FC = () => {
const { control, setValue } = useFormContext<FormValues>();
const kind = useWatch({ control, name: 'kind' });
const stateTransition = useWatch({ control, name: 'stateTransition' });
const [selectedMode, setSelectedMode] = useState<DelayMode>(deriveMode(stateTransition));

const onModeChange = useCallback(
(mode: string) => {
switch (mode as DelayMode) {
case 'immediate':
setSelectedMode('immediate');
setValue('stateTransition', undefined);
break;
case 'breaches':
setSelectedMode('breaches');
setValue(
'stateTransition.pendingCount',
stateTransition?.pendingCount ?? DEFAULT_PENDING_COUNT
);
setValue('stateTransition.pendingTimeframe', undefined);
break;
case 'duration':
setSelectedMode('duration');
setValue('stateTransition.pendingCount', undefined);
setValue(
'stateTransition.pendingTimeframe',
stateTransition?.pendingTimeframe ?? DEFAULT_PENDING_TIMEFRAME
);
break;
}
},
[setValue, stateTransition?.pendingCount, stateTransition?.pendingTimeframe]
);

if (kind !== 'alert') {
return null;
}

return (
<FieldGroup
title={i18n.translate('xpack.alertingV2.ruleForm.stateTransition.title', {
defaultMessage: 'Alert delay',
})}
>
<EuiButtonGroup
buttonSize="s"
legend={i18n.translate('xpack.alertingV2.ruleForm.stateTransition.delayModeLegend', {
defaultMessage: 'Alert delay mode',
})}
options={MODE_OPTIONS}
idSelected={selectedMode}
onChange={onModeChange}
isFullWidth
data-test-subj="stateTransitionDelayMode"
/>
<EuiSpacer size="s" />
{selectedMode === 'immediate' && (
<EuiText size="xs" color="subdued" data-test-subj="stateTransitionImmediateDescription">
{i18n.translate('xpack.alertingV2.ruleForm.stateTransition.immediateDescription', {
defaultMessage: 'No delay - Alerts on first breach',
})}
</EuiText>
)}
{selectedMode === 'breaches' && (
<StateTransitionCountField
prependLabel={i18n.translate(
'xpack.alertingV2.ruleForm.stateTransition.inlineBreachesPrepend',
{ defaultMessage: 'Consecutive breaches' }
)}
/>
)}
{selectedMode === 'duration' && (
<StateTransitionTimeframeField
numberPrependLabel={i18n.translate(
'xpack.alertingV2.ruleForm.stateTransition.inlineDurationPrepend',
{ defaultMessage: 'Active for' }
)}
/>
)}
</FieldGroup>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ export const DescriptionField: React.FC = () => {
label={i18n.translate('xpack.alertingV2.ruleForm.descriptionLabel', {
defaultMessage: 'Description',
})}
fullWidth
isInvalid={!!error}
error={error?.message}
>
<EuiTextArea
{...field}
inputRef={ref}
rows={2}
fullWidth
isInvalid={!!error}
placeholder={i18n.translate('xpack.alertingV2.ruleForm.descriptionPlaceholder', {
defaultMessage: 'Add an optional description for this rule...',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ export const GroupFieldSelect: React.FC = () => {
label={i18n.translate('xpack.alertingV2.ruleForm.groupingKeyLabel', {
defaultMessage: 'Group Fields',
})}
fullWidth
isInvalid={!!error}
error={error?.message}
>
<EuiComboBox
id={groupByRowId}
fullWidth
options={options}
selectedOptions={selectedOptions}
onChange={(selected) => field.onChange(selected.map(({ label }) => label))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const KindField: React.FC = () => {
helpText={i18n.translate('xpack.alertingV2.ruleForm.kindHelpText', {
defaultMessage: 'Choose whether this rule creates monitors or alerts.',
})}
fullWidth
isInvalid={!!error}
error={error?.message}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import React, { useMemo, useCallback } from 'react';
import { EuiFlexItem, EuiFormRow, EuiFlexGroup, EuiSelect, EuiFieldNumber } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getDurationUnitValue, getDurationNumberInItsUnit } from '../../flyout/utils';
import { getTimeOptions } from '../../flyout/utils';

const INTEGER_REGEX = /^[1-9][0-9]*$/;
const INVALID_KEYS = ['-', '+', '.', 'e', 'E'];
import {
getDurationUnitValue,
getDurationNumberInItsUnit,
getTimeOptions,
INVALID_NUMBER_KEYS,
parsePositiveIntegerInput,
} from '../utils';

const LOOKBACK_WINDOW_TITLE_PREFIX = i18n.translate(
'xpack.alertingV2.ruleForm.lookbackWindow.titlePrefix',
Expand Down Expand Up @@ -46,9 +48,8 @@ export const LookbackWindow: React.FC<Props> = React.forwardRef<HTMLInputElement

const onIntervalNumberChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value.trim();
if (INTEGER_REGEX.test(val)) {
const parsedValue = parseInt(val, 10);
const parsedValue = parsePositiveIntegerInput(e.target.value);
if (parsedValue != null) {
onChange(`${parsedValue}${intervalUnit}`);
}
},
Expand All @@ -63,7 +64,7 @@ export const LookbackWindow: React.FC<Props> = React.forwardRef<HTMLInputElement
);

const onKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (INVALID_KEYS.includes(e.key)) {
if (INVALID_NUMBER_KEYS.includes(e.key)) {
e.preventDefault();
}
}, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const LookbackWindowField: React.FC = () => {
label={i18n.translate('xpack.alertingV2.ruleForm.lookbackWindowLabel', {
defaultMessage: 'Lookback Window',
})}
fullWidth
isInvalid={!!error}
error={error?.message}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ export const NameField: React.FC = () => {
label={i18n.translate('xpack.alertingV2.ruleForm.nameLabel', {
defaultMessage: 'Name',
})}
fullWidth
isInvalid={!!error}
error={error?.message}
>
<EuiFieldText {...field} inputRef={ref} isInvalid={!!error} />
<EuiFieldText {...field} inputRef={ref} isInvalid={!!error} fullWidth />
</EuiFormRow>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import React, { useMemo, useCallback } from 'react';
import { EuiFlexItem, EuiFormRow, EuiFlexGroup, EuiSelect, EuiFieldNumber } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getDurationUnitValue, getDurationNumberInItsUnit } from '../../flyout/utils';
import { getTimeOptions } from '../../flyout/utils';

const INTEGER_REGEX = /^[1-9][0-9]*$/;
const INVALID_KEYS = ['-', '+', '.', 'e', 'E'];
import {
getDurationUnitValue,
getDurationNumberInItsUnit,
getTimeOptions,
INVALID_NUMBER_KEYS,
parsePositiveIntegerInput,
} from '../utils';

const SCHEDULE_TITLE_PREFIX = i18n.translate('xpack.alertingV2.ruleForm.schedule.titlePrefix', {
defaultMessage: 'Every',
Expand Down Expand Up @@ -40,9 +42,8 @@ export const RuleSchedule: React.FC<Props> = React.forwardRef<HTMLInputElement,

const onIntervalNumberChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value.trim();
if (INTEGER_REGEX.test(val)) {
const parsedValue = parseInt(val, 10);
const parsedValue = parsePositiveIntegerInput(e.target.value);
if (parsedValue != null) {
onChange(`${parsedValue}${intervalUnit}`);
}
},
Expand All @@ -57,7 +58,7 @@ export const RuleSchedule: React.FC<Props> = React.forwardRef<HTMLInputElement,
);

const onKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (INVALID_KEYS.includes(e.key)) {
if (INVALID_NUMBER_KEYS.includes(e.key)) {
e.preventDefault();
}
}, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const ScheduleField: React.FC = () => {
label={i18n.translate('xpack.alertingV2.ruleForm.scheduleLabel', {
defaultMessage: 'Schedule',
})}
fullWidth
helpText={
<>
{i18n.translate('xpack.alertingV2.ruleForm.scheduleHelpText', {
Expand Down
Loading
Loading