Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3707793
Basic list notification policies UI
kdelemme Mar 2, 2026
e7ccfd5
Add react query hook
kdelemme Mar 2, 2026
10fb46d
Add basic form
kdelemme Mar 2, 2026
91f3116
Improve UI with panel header
kdelemme Mar 2, 2026
75b70c6
Move to form/
kdelemme Mar 2, 2026
eb51143
Handle multiple destination workflow
kdelemme Mar 2, 2026
73d6185
wrap destination badges
kdelemme Mar 2, 2026
ac8c997
remove exported type
kdelemme Mar 2, 2026
72558e8
Handle edit action
kdelemme Mar 2, 2026
4ee282f
Add delete action
kdelemme Mar 2, 2026
1104634
Add storybook
kdelemme Mar 2, 2026
ef4a20e
Changes from node scripts/lint_ts_projects --fix
kibanamachine Mar 2, 2026
d0df360
Add unit tests
kdelemme Mar 2, 2026
88cdd01
Remove uncontrolled input warning
kdelemme Mar 2, 2026
f411657
Address comments
kdelemme Mar 2, 2026
309a813
Add validation for destinations
kdelemme Mar 2, 2026
83de86f
Add form page
kdelemme Mar 3, 2026
0a5fdf6
Move flyout into folder
kdelemme Mar 3, 2026
4552f20
Merge branch 'alerting_v2' into alertingv2/notification-policies-ui
kdelemme Mar 3, 2026
621c553
fix integration test setup
kdelemme Mar 3, 2026
2c32656
Merge branch 'alerting_v2' into alertingv2/notification-policies-ui
kdelemme Mar 3, 2026
d3ac599
Changes from node scripts/lint_ts_projects --fix
kibanamachine Mar 3, 2026
329137e
Handle use params correctly in tests
kdelemme Mar 4, 2026
e5f4b41
Add group and throttle
kdelemme Mar 4, 2026
6d4080b
remove throttle use update
kdelemme Mar 4, 2026
c9d9037
cherry pick dispatcher integration test
kdelemme Mar 4, 2026
a170ea3
Extract common form logic
kdelemme Mar 4, 2026
baba259
Merge branch 'alerting_v2' into alertingv2/notification-policies-ui
kdelemme Mar 4, 2026
cb77d23
Remove requried for description
kdelemme 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
1 change: 1 addition & 0 deletions src/dev/storybook/aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

export const storybookAliases = {
ai_assistant: 'x-pack/platform/packages/shared/kbn-ai-assistant/.storybook',
alerting_v2: 'x-pack/platform/plugins/shared/alerting_v2/.storybook',
alerting_v2_rule_form:
'x-pack/platform/packages/shared/response-ops/alerting-v2-rule-form/.storybook',
apm: 'x-pack/solutions/observability/plugins/apm/.storybook',
Expand Down
8 changes: 8 additions & 0 deletions x-pack/platform/plugins/shared/alerting_v2/.storybook/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

module.exports = require('@kbn/storybook').defaultConfig;
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import React from 'react';
import { Route, Routes } from '@kbn/shared-ux-router';
import { CreateRulePage } from './create_rule_page';
import { RulesListPage } from './rules_list_page';
import { ListNotificationPoliciesPage } from '../pages/list_notification_policies_page/list_notification_policies_page';
import { NotificationPolicyFormPage } from '../pages/notification_policy_form_page/notification_policy_form_page';

export const App = () => {
return (
Expand All @@ -19,6 +21,15 @@ export const App = () => {
<Route path="/create">
<CreateRulePage />
</Route>
<Route path="/notification_policies/create">
<NotificationPolicyFormPage />
</Route>
<Route path="/notification_policies/edit/:id">
<NotificationPolicyFormPage />
</Route>
<Route path="/notification_policies">
<ListNotificationPoliciesPage />
</Route>
<Route path="/">
<RulesListPage />
</Route>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 { EuiConfirmModal, useGeneratedHtmlId } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';

interface DeleteNotificationPolicyConfirmModalProps {
policyName: string;
onCancel: () => void;
onConfirm: () => void;
isLoading?: boolean;
}

export const DeleteNotificationPolicyConfirmModal = ({
policyName,
onCancel,
onConfirm,
isLoading = false,
}: DeleteNotificationPolicyConfirmModalProps) => {
const titleId = useGeneratedHtmlId();
return (
<EuiConfirmModal
id="deleteNotificationPolicyConfirmModal"
aria-labelledby={titleId}
titleProps={{ id: titleId }}
title={i18n.translate('xpack.alertingV2.notificationPolicy.deleteModal.title', {
defaultMessage: 'Delete notification policy',
})}
onCancel={onCancel}
onConfirm={onConfirm}
cancelButtonText={i18n.translate(
'xpack.alertingV2.notificationPolicy.deleteModal.cancelButton',
{ defaultMessage: 'Cancel' }
)}
confirmButtonText={i18n.translate(
'xpack.alertingV2.notificationPolicy.deleteModal.confirmButton',
{ defaultMessage: 'Delete' }
)}
buttonColor="danger"
isLoading={isLoading}
>
<FormattedMessage
id="xpack.alertingV2.notificationPolicy.deleteModal.body"
defaultMessage="Are you sure you want to delete {policyName}? This action cannot be undone."
values={{ policyName: <strong>{policyName}</strong> }}
/>
</EuiConfirmModal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { NotificationPolicyFormState } from './types';

export const WORKFLOW_OPTIONS = [
{ value: 'workflow-1', label: 'Slack notification workflow' },
{ value: 'workflow-2', label: 'PagerDuty escalation workflow' },
{ value: 'workflow-3', label: 'Email digest workflow' },
];
Comment on lines +11 to +15
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.

💬 dummies for now


export const FREQUENCY_OPTIONS = [
{
value: 'immediate',
text: i18n.translate('xpack.alertingV2.notificationPolicy.form.frequency.immediate', {
defaultMessage: 'Immediate',
}),
},
{
value: 'throttle',
text: i18n.translate('xpack.alertingV2.notificationPolicy.form.frequency.throttle', {
defaultMessage: 'Throttle',
}),
},
];

export const THROTTLE_INTERVAL_PATTERN = /^[1-9][0-9]*[dhms]$/;

export const DEFAULT_FORM_STATE: NotificationPolicyFormState = {
name: '',
description: '',
matcher: '',
groupBy: [],
frequency: { type: 'immediate' },
destinations: [{ type: 'workflow', id: 'workflow-1' }],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 type {
CreateNotificationPolicyData,
NotificationPolicyResponse,
UpdateNotificationPolicyBody,
} from '@kbn/alerting-v2-schemas';
import type { NotificationPolicyFormState } from './types';

export const toFormState = (response: NotificationPolicyResponse): NotificationPolicyFormState => {
return {
name: response.name,
description: response.description,
matcher: response.matcher ?? '',
groupBy: response.group_by ?? [],
frequency: response.throttle
? { type: 'throttle', interval: response.throttle.interval }
: { type: 'immediate' },
destinations: response.destinations.map((d) => ({ type: d.type, id: d.id })),
};
};

export const toCreatePayload = (
state: NotificationPolicyFormState
): CreateNotificationPolicyData => {
return {
name: state.name,
description: state.description,
...(state.matcher ? { matcher: state.matcher } : {}),
...(state.groupBy.length > 0 ? { group_by: state.groupBy } : {}),
...(state.frequency.type === 'throttle'
? { throttle: { interval: state.frequency.interval } }
: {}),
destinations: state.destinations.map((d) => ({ type: d.type, id: d.id })),
};
};

export const toUpdatePayload = (
state: NotificationPolicyFormState,
version: string
): UpdateNotificationPolicyBody => {
return {
...toCreatePayload(state),
version,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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.
*/

export { NotificationPolicyFormFlyout } from '../form_flyout/notification_policy_form_flyout';
export { toFormState, toCreatePayload, toUpdatePayload } from './form_utils';
export { useNotificationPolicyForm } from './use_notification_policy_form';
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 type { Meta, StoryObj } from '@storybook/react';
import { FormProvider, useForm } from 'react-hook-form';
import { DEFAULT_FORM_STATE } from './constants';
import { NotificationPolicyForm } from './notification_policy_form';
import type { NotificationPolicyFormState } from './types';

interface NotificationPolicyFormStoryProps {
defaultValues: NotificationPolicyFormState;
}

const NotificationPolicyFormStory = ({ defaultValues }: NotificationPolicyFormStoryProps) => {
const methods = useForm<NotificationPolicyFormState>({
mode: 'onBlur',
defaultValues,
});

return (
<FormProvider {...methods}>
<NotificationPolicyForm />
</FormProvider>
);
};

const meta: Meta<typeof NotificationPolicyFormStory> = {
title: 'Alerting V2/Notification Policy/Form',
component: NotificationPolicyFormStory,
parameters: {
layout: 'padded',
},
};

export default meta;

type Story = StoryObj<typeof NotificationPolicyFormStory>;

export const CreateMode: Story = {
args: {
defaultValues: {
...DEFAULT_FORM_STATE,
groupBy: [...DEFAULT_FORM_STATE.groupBy],
frequency: { ...DEFAULT_FORM_STATE.frequency },
destinations: DEFAULT_FORM_STATE.destinations.map((destination) => ({ ...destination })),
},
},
};

export const EditMode: Story = {
args: {
defaultValues: {
name: 'Critical production alerts',
description: 'Routes critical production alerts to escalation workflows',
matcher: 'data.severity : "critical" and data.env : "prod"',
groupBy: ['host.name', 'service.name'],
frequency: { type: 'throttle', interval: '5m' },
destinations: [{ type: 'workflow', id: 'workflow-2' }],
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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 { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { I18nProvider } from '@kbn/i18n-react';
import { FormProvider, useForm } from 'react-hook-form';
import { DEFAULT_FORM_STATE } from './constants';
import { NotificationPolicyForm } from './notification_policy_form';
import type { NotificationPolicyFormState } from './types';

const renderForm = (defaultValues: NotificationPolicyFormState = DEFAULT_FORM_STATE) => {
const TestComponent = () => {
const methods = useForm<NotificationPolicyFormState>({
mode: 'onBlur',
defaultValues,
});

return (
<I18nProvider>
<FormProvider {...methods}>
<NotificationPolicyForm />
</FormProvider>
</I18nProvider>
);
};

return render(<TestComponent />);
};

const TEST_SUBJ = {
nameInput: 'nameInput',
descriptionInput: 'descriptionInput',
frequencySelect: 'frequencySelect',
throttleIntervalInput: 'throttleIntervalInput',
} as const;

describe('NotificationPolicyForm', () => {
it('shows required errors for name on blur', async () => {
const user = userEvent.setup();
renderForm();

await user.click(screen.getByTestId(TEST_SUBJ.nameInput));
await user.tab();
expect(await screen.findByText('Name is required.')).toBeInTheDocument();
});

it('shows throttle interval input only when throttle frequency is selected', async () => {
const user = userEvent.setup();
renderForm();

expect(screen.queryByTestId(TEST_SUBJ.throttleIntervalInput)).not.toBeInTheDocument();

await user.selectOptions(screen.getByTestId(TEST_SUBJ.frequencySelect), 'throttle');

expect(screen.getByTestId(TEST_SUBJ.throttleIntervalInput)).toBeInTheDocument();
});

it('validates throttle interval format when in throttle mode', async () => {
const user = userEvent.setup();
renderForm();

await user.selectOptions(screen.getByTestId(TEST_SUBJ.frequencySelect), 'throttle');

const intervalInput = screen.getByTestId(TEST_SUBJ.throttleIntervalInput);
await user.clear(intervalInput);
await user.type(intervalInput, '10x');
await user.tab();

expect(
await screen.findByText('Invalid throttle interval. Must be in the format of 1h, 5m, 30s')
).toBeInTheDocument();
});
});
Loading
Loading