setPolicyToDelete(null)}
+ onConfirm={() => {
+ deleteNotificationPolicy(policyToDelete.id, {
+ onSuccess: () => setPolicyToDelete(null),
+ });
+ }}
+ isLoading={isDeleting}
+ />
+ )}
+ >
+ );
+};
diff --git a/x-pack/platform/plugins/shared/alerting_v2/public/pages/notification_policy_form_page/notification_policy_form_page.test.tsx b/x-pack/platform/plugins/shared/alerting_v2/public/pages/notification_policy_form_page/notification_policy_form_page.test.tsx
new file mode 100644
index 0000000000000..b433914891f98
--- /dev/null
+++ b/x-pack/platform/plugins/shared/alerting_v2/public/pages/notification_policy_form_page/notification_policy_form_page.test.tsx
@@ -0,0 +1,256 @@
+/*
+ * 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, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import type { NotificationPolicyResponse } from '@kbn/alerting-v2-schemas';
+import { I18nProvider } from '@kbn/i18n-react';
+import { NotificationPolicyFormPage } from './notification_policy_form_page';
+
+const mockNavigateToUrl = jest.fn();
+const mockBasePath = { prepend: jest.fn((path: string) => `/mock${path}`) };
+
+jest.mock('@kbn/core-di-browser', () => ({
+ useService: jest.fn((token: unknown) => {
+ const tokenStr = String(token);
+ if (tokenStr.includes('application')) {
+ return { navigateToUrl: mockNavigateToUrl };
+ }
+ if (tokenStr.includes('http')) {
+ return { basePath: mockBasePath };
+ }
+ return {};
+ }),
+ CoreStart: jest.fn((name: string) => `CoreStart(${name})`),
+}));
+
+const mockCreateMutate = jest.fn();
+const mockUpdateMutate = jest.fn();
+
+jest.mock('../../hooks/use_create_notification_policy', () => ({
+ useCreateNotificationPolicy: () => ({
+ mutate: mockCreateMutate,
+ isLoading: false,
+ }),
+}));
+
+jest.mock('../../hooks/use_update_notification_policy', () => ({
+ useUpdateNotificationPolicy: () => ({
+ mutate: mockUpdateMutate,
+ isLoading: false,
+ }),
+}));
+
+const mockUseFetchNotificationPolicy = jest.fn();
+jest.mock('../../hooks/use_fetch_notification_policy', () => ({
+ useFetchNotificationPolicy: (...args: unknown[]) => mockUseFetchNotificationPolicy(...args),
+}));
+
+const mockUseParams = jest.fn();
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => mockUseParams(),
+}));
+
+const TEST_SUBJ = {
+ pageTitle: 'pageTitle',
+ cancelButton: 'cancelButton',
+ submitButton: 'submitButton',
+ nameInput: 'nameInput',
+ descriptionInput: 'descriptionInput',
+ loadingSpinner: 'loadingSpinner',
+ fetchErrorCallout: 'fetchErrorCallout',
+} as const;
+
+const EXISTING_POLICY: NotificationPolicyResponse = {
+ id: 'policy-1',
+ version: 'WzEsMV0=',
+ name: 'Critical production alerts',
+ description: 'Routes critical alerts',
+ matcher: 'data.severity : "critical"',
+ group_by: ['host.name', 'service.name'],
+ throttle: { interval: '5m' },
+ destinations: [{ type: 'workflow', id: 'workflow-2' }],
+ createdBy: 'elastic',
+ createdAt: '2026-03-01T10:00:00.000Z',
+ updatedBy: 'elastic',
+ updatedAt: '2026-03-01T10:00:00.000Z',
+};
+
+const renderPage = () => {
+ return render(
+
+
+
+ );
+};
+
+describe('NotificationPolicyFormPage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseFetchNotificationPolicy.mockReturnValue({
+ data: undefined,
+ isLoading: false,
+ isError: false,
+ error: null,
+ });
+ });
+
+ describe('create mode', () => {
+ beforeEach(() => {
+ mockUseParams.mockReturnValue({});
+ });
+
+ it('renders create title and save button', () => {
+ renderPage();
+
+ expect(screen.getByTestId(TEST_SUBJ.pageTitle)).toHaveTextContent(
+ 'Create notification policy'
+ );
+ expect(screen.getByTestId(TEST_SUBJ.submitButton)).toHaveTextContent('Save');
+ });
+
+ it('submits create payload on save', async () => {
+ const user = userEvent.setup();
+ renderPage();
+
+ await user.type(screen.getByTestId(TEST_SUBJ.nameInput), 'Policy from test');
+ await user.tab();
+ await user.type(screen.getByTestId(TEST_SUBJ.descriptionInput), 'Description from test');
+ await user.tab();
+
+ const saveButton = screen.getByTestId(TEST_SUBJ.submitButton);
+ await waitFor(() => expect(saveButton).toBeEnabled());
+ await user.click(saveButton);
+
+ expect(mockCreateMutate).toHaveBeenCalledTimes(1);
+ expect(mockCreateMutate).toHaveBeenCalledWith(
+ {
+ name: 'Policy from test',
+ description: 'Description from test',
+ destinations: [{ type: 'workflow', id: 'workflow-1' }],
+ },
+ expect.objectContaining({ onSuccess: expect.any(Function) })
+ );
+ });
+
+ it('navigates to listing page on cancel', async () => {
+ const user = userEvent.setup();
+ renderPage();
+
+ await user.click(screen.getByTestId(TEST_SUBJ.cancelButton));
+
+ expect(mockNavigateToUrl).toHaveBeenCalledWith(
+ expect.stringContaining('/notification_policies')
+ );
+ });
+ });
+
+ describe('edit mode', () => {
+ beforeEach(() => {
+ mockUseParams.mockReturnValue({ id: 'policy-1' });
+ });
+
+ it('renders edit title and update button when policy is loaded', () => {
+ mockUseFetchNotificationPolicy.mockReturnValue({
+ data: EXISTING_POLICY,
+ isLoading: false,
+ isError: false,
+ error: null,
+ });
+
+ renderPage();
+
+ expect(screen.getByTestId(TEST_SUBJ.pageTitle)).toHaveTextContent('Edit notification policy');
+ expect(screen.getByTestId(TEST_SUBJ.submitButton)).toHaveTextContent('Update');
+ });
+
+ it('shows loading state while fetching', () => {
+ mockUseFetchNotificationPolicy.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ isError: false,
+ error: null,
+ });
+
+ renderPage();
+
+ expect(screen.getByTestId(TEST_SUBJ.loadingSpinner)).toBeInTheDocument();
+ });
+
+ it('shows error callout when fetch fails', () => {
+ mockUseFetchNotificationPolicy.mockReturnValue({
+ data: undefined,
+ isLoading: false,
+ isError: true,
+ error: new Error('Not found'),
+ });
+
+ renderPage();
+
+ expect(screen.getByTestId(TEST_SUBJ.fetchErrorCallout)).toBeInTheDocument();
+ expect(screen.getByText('Not found')).toBeInTheDocument();
+ });
+
+ it('submits update payload on save', async () => {
+ const user = userEvent.setup();
+ mockUseFetchNotificationPolicy.mockReturnValue({
+ data: EXISTING_POLICY,
+ isLoading: false,
+ isError: false,
+ error: null,
+ });
+
+ renderPage();
+
+ await user.click(screen.getByTestId(TEST_SUBJ.nameInput));
+ await user.tab();
+ await user.click(screen.getByTestId(TEST_SUBJ.descriptionInput));
+ await user.tab();
+
+ const updateButton = screen.getByTestId(TEST_SUBJ.submitButton);
+ await waitFor(() => expect(updateButton).toBeEnabled());
+ await user.click(updateButton);
+
+ expect(mockUpdateMutate).toHaveBeenCalledTimes(1);
+ expect(mockUpdateMutate).toHaveBeenCalledWith(
+ {
+ id: 'policy-1',
+ data: {
+ version: 'WzEsMV0=',
+ name: 'Critical production alerts',
+ description: 'Routes critical alerts',
+ matcher: 'data.severity : "critical"',
+ group_by: ['host.name', 'service.name'],
+ throttle: { interval: '5m' },
+ destinations: [{ type: 'workflow', id: 'workflow-2' }],
+ },
+ },
+ expect.objectContaining({ onSuccess: expect.any(Function) })
+ );
+ });
+
+ it('navigates to listing page on cancel', async () => {
+ const user = userEvent.setup();
+ mockUseFetchNotificationPolicy.mockReturnValue({
+ data: EXISTING_POLICY,
+ isLoading: false,
+ isError: false,
+ error: null,
+ });
+
+ renderPage();
+
+ await user.click(screen.getByTestId(TEST_SUBJ.cancelButton));
+
+ expect(mockNavigateToUrl).toHaveBeenCalledWith(
+ expect.stringContaining('/notification_policies')
+ );
+ });
+ });
+});
diff --git a/x-pack/platform/plugins/shared/alerting_v2/public/pages/notification_policy_form_page/notification_policy_form_page.tsx b/x-pack/platform/plugins/shared/alerting_v2/public/pages/notification_policy_form_page/notification_policy_form_page.tsx
new file mode 100644
index 0000000000000..c1601282e7ce5
--- /dev/null
+++ b/x-pack/platform/plugins/shared/alerting_v2/public/pages/notification_policy_form_page/notification_policy_form_page.tsx
@@ -0,0 +1,198 @@
+/*
+ * 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 {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiCallOut,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingSpinner,
+ EuiPageHeader,
+ EuiSpacer,
+} from '@elastic/eui';
+import type {
+ CreateNotificationPolicyData,
+ NotificationPolicyResponse,
+ UpdateNotificationPolicyBody,
+} from '@kbn/alerting-v2-schemas';
+import { CoreStart, useService } from '@kbn/core-di-browser';
+import { FormattedMessage } from '@kbn/i18n-react';
+import React, { useCallback } from 'react';
+import { FormProvider } from 'react-hook-form';
+import { useParams } from 'react-router-dom';
+import { NotificationPolicyForm } from '../../components/notification_policy/form/notification_policy_form';
+import { useNotificationPolicyForm } from '../../components/notification_policy/form/use_notification_policy_form';
+import { paths } from '../../constants';
+import { useCreateNotificationPolicy } from '../../hooks/use_create_notification_policy';
+import { useFetchNotificationPolicy } from '../../hooks/use_fetch_notification_policy';
+import { useUpdateNotificationPolicy } from '../../hooks/use_update_notification_policy';
+
+export const NotificationPolicyFormPage = () => {
+ const { id: policyId } = useParams<{ id?: string }>();
+ const { navigateToUrl } = useService(CoreStart('application'));
+ const { basePath } = useService(CoreStart('http'));
+
+ const {
+ data: existingPolicy,
+ isLoading: isFetchingPolicy,
+ isError: isFetchError,
+ error: fetchError,
+ } = useFetchNotificationPolicy(policyId);
+
+ const isEditMode = !!policyId;
+ const isReady = !isEditMode || !!existingPolicy;
+
+ const navigateToList = useCallback(() => {
+ navigateToUrl(basePath.prepend(paths.notificationPolicyList));
+ }, [navigateToUrl, basePath]);
+
+ if (isEditMode && isFetchingPolicy) {
+ return (
+ <>
+
+ }
+ />
+
+
+
+
+ >
+ );
+ }
+
+ if (isEditMode && isFetchError) {
+ return (
+ <>
+
+ }
+ />
+
+
+ }
+ color="danger"
+ iconType="error"
+ data-test-subj="fetchErrorCallout"
+ >
+ {fetchError?.message}
+
+ >
+ );
+ }
+
+ if (!isReady) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+const NotificationPolicyFormPageContent = ({
+ initialPolicy,
+ onCancel,
+ onSuccess,
+}: {
+ initialPolicy?: NotificationPolicyResponse;
+ onCancel: () => void;
+ onSuccess: () => void;
+}) => {
+ const { mutate: createPolicy, isLoading: isCreating } = useCreateNotificationPolicy();
+ const { mutate: updatePolicy, isLoading: isUpdating } = useUpdateNotificationPolicy();
+
+ const onSubmitCreate = (data: CreateNotificationPolicyData) => createPolicy(data, { onSuccess });
+ const onSubmitUpdate = (id: string, data: UpdateNotificationPolicyBody) =>
+ updatePolicy({ id, data }, { onSuccess });
+
+ const { methods, isEditMode, handleSubmit } = useNotificationPolicyForm({
+ initialValues: initialPolicy,
+ onSubmitCreate,
+ onSubmitUpdate,
+ });
+
+ const isLoading = isCreating || isUpdating;
+
+ return (
+ <>
+
+ ) : (
+
+ )
+ }
+ data-test-subj="pageTitle"
+ />
+
+
+
+
+
+
+
+
+
+ {isEditMode ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/platform/plugins/shared/alerting_v2/public/services/notification_policies_api.ts b/x-pack/platform/plugins/shared/alerting_v2/public/services/notification_policies_api.ts
new file mode 100644
index 0000000000000..02bf77ad6c83c
--- /dev/null
+++ b/x-pack/platform/plugins/shared/alerting_v2/public/services/notification_policies_api.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { inject, injectable } from 'inversify';
+import type { HttpStart } from '@kbn/core/public';
+import { CoreStart } from '@kbn/core-di-browser';
+import type {
+ CreateNotificationPolicyData,
+ NotificationPolicyResponse,
+ UpdateNotificationPolicyBody,
+} from '@kbn/alerting-v2-schemas';
+import { INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH } from '../constants';
+
+export interface FindNotificationPoliciesResponse {
+ items: NotificationPolicyResponse[];
+ total: number;
+ page: number;
+ perPage: number;
+}
+
+@injectable()
+export class NotificationPoliciesApi {
+ constructor(@inject(CoreStart('http')) private readonly http: HttpStart) {}
+
+ public async getNotificationPolicy(id: string) {
+ return this.http.get(
+ `${INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH}/${id}`
+ );
+ }
+
+ public async listNotificationPolicies(params: { page?: number; perPage?: number }) {
+ return this.http.get(
+ INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH,
+ { query: { page: params.page, perPage: params.perPage } }
+ );
+ }
+
+ public async createNotificationPolicy(data: CreateNotificationPolicyData) {
+ return this.http.post(
+ INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH,
+ { body: JSON.stringify(data) }
+ );
+ }
+
+ public async updateNotificationPolicy(id: string, data: UpdateNotificationPolicyBody) {
+ return this.http.put(
+ `${INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH}/${id}`,
+ { body: JSON.stringify(data) }
+ );
+ }
+
+ public async deleteNotificationPolicy(id: string) {
+ await this.http.delete(`${INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH}/${id}`);
+ }
+}
diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts
index 291235d5cd7b5..855dd06f63a4a 100644
--- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts
+++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts
@@ -6,4 +6,9 @@
*/
export { NotificationPolicyClient } from './notification_policy_client';
-export type { CreateNotificationPolicyParams, UpdateNotificationPolicyParams } from './types';
+export type {
+ CreateNotificationPolicyParams,
+ FindNotificationPoliciesParams,
+ FindNotificationPoliciesResponse,
+ UpdateNotificationPolicyParams,
+} from './types';
diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts
index dadfaaa8c4f68..5064acf51f595 100644
--- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts
+++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts
@@ -22,7 +22,12 @@ import type { NotificationPolicySavedObjectServiceContract } from '../services/n
import { NotificationPolicySavedObjectServiceScopedToken } from '../services/notification_policy_saved_object_service/tokens';
import type { UserServiceContract } from '../services/user_service/user_service';
import { UserService } from '../services/user_service/user_service';
-import type { CreateNotificationPolicyParams, UpdateNotificationPolicyParams } from './types';
+import type {
+ CreateNotificationPolicyParams,
+ FindNotificationPoliciesParams,
+ FindNotificationPoliciesResponse,
+ UpdateNotificationPolicyParams,
+} from './types';
const toAuthResponse = (
auth: NotificationPolicySavedObjectAttributes['auth']
@@ -175,6 +180,26 @@ export class NotificationPolicyClient {
}
}
+ public async findNotificationPolicies(
+ params: FindNotificationPoliciesParams = {}
+ ): Promise {
+ const page = params.page ?? 1;
+ const perPage = params.perPage ?? 20;
+
+ const res = await this.notificationPolicySavedObjectService.find({ page, perPage });
+
+ return {
+ items: res.saved_objects.map((so) => ({
+ id: so.id,
+ version: so.version,
+ ...so.attributes,
+ })),
+ total: res.total,
+ page,
+ perPage,
+ };
+ }
+
public async deleteNotificationPolicy({ id }: { id: string }): Promise {
await this.getNotificationPolicy({ id });
await this.notificationPolicySavedObjectService.delete({ id });
diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts
index 8465df2209cf7..1512720393bdb 100644
--- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts
+++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts
@@ -7,6 +7,7 @@
import type {
CreateNotificationPolicyData,
+ NotificationPolicyResponse,
UpdateNotificationPolicyData,
} from '@kbn/alerting-v2-schemas';
@@ -19,3 +20,15 @@ export interface CreateNotificationPolicyParams {
data: CreateNotificationPolicyData;
options?: { id?: string };
}
+
+export interface FindNotificationPoliciesParams {
+ page?: number;
+ perPage?: number;
+}
+
+export interface FindNotificationPoliciesResponse {
+ items: NotificationPolicyResponse[];
+ total: number;
+ page: number;
+ perPage: number;
+}
diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts
index 6b36fe1d38d0c..43ba392677e76 100644
--- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts
+++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts
@@ -48,6 +48,14 @@ export interface NotificationPolicySavedObjectServiceContract {
version: string;
}): Promise<{ id: string; version?: string }>;
delete(params: { id: string }): Promise;
+ find(params: { page: number; perPage: number }): Promise<{
+ saved_objects: Array<{
+ id: string;
+ attributes: NotificationPolicySavedObjectAttributes;
+ version?: string;
+ }>;
+ total: number;
+ }>;
}
@injectable()
@@ -153,4 +161,14 @@ export class NotificationPolicySavedObjectService
public async delete({ id }: { id: string }): Promise {
await this.client.delete(NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, id);
}
+
+ public async find({ page, perPage }: { page: number; perPage: number }) {
+ return this.client.find({
+ type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE,
+ page,
+ perPage,
+ sortField: 'updatedAt',
+ sortOrder: 'desc',
+ });
+ }
}
diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/list_notification_policies_route.ts b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/list_notification_policies_route.ts
new file mode 100644
index 0000000000000..f5a03595bddbf
--- /dev/null
+++ b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/list_notification_policies_route.ts
@@ -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 Boom from '@hapi/boom';
+import { schema } from '@kbn/config-schema';
+import type { TypeOf } from '@kbn/config-schema';
+import { Request, Response } from '@kbn/core-di-server';
+import type { KibanaRequest, KibanaResponseFactory, RouteSecurity } from '@kbn/core-http-server';
+import { inject, injectable } from 'inversify';
+import { NotificationPolicyClient } from '../../lib/notification_policy_client';
+import { ALERTING_V2_API_PRIVILEGES } from '../../lib/security/privileges';
+import { INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH } from '../constants';
+
+const listNotificationPoliciesQuerySchema = schema.object({
+ page: schema.maybe(schema.number({ min: 1 })),
+ perPage: schema.maybe(schema.number({ min: 1, max: 100 })),
+});
+
+@injectable()
+export class ListNotificationPoliciesRoute {
+ static method = 'get' as const;
+ static path = `${INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH}`;
+ static security: RouteSecurity = {
+ authz: {
+ requiredPrivileges: [ALERTING_V2_API_PRIVILEGES.notificationPolicies.read],
+ },
+ };
+ static options = { access: 'internal' } as const;
+ static validate = {
+ request: {
+ query: listNotificationPoliciesQuerySchema,
+ },
+ } as const;
+
+ constructor(
+ @inject(Request)
+ private readonly request: KibanaRequest<
+ unknown,
+ TypeOf,
+ unknown
+ >,
+ @inject(Response) private readonly response: KibanaResponseFactory,
+ @inject(NotificationPolicyClient)
+ private readonly notificationPolicyClient: NotificationPolicyClient
+ ) {}
+
+ async handle() {
+ try {
+ const result = await this.notificationPolicyClient.findNotificationPolicies({
+ page: this.request.query?.page,
+ perPage: this.request.query?.perPage,
+ });
+ return this.response.ok({ body: result });
+ } catch (e) {
+ const boom = Boom.isBoom(e) ? e : Boom.boomify(e);
+ return this.response.customError({
+ statusCode: boom.output.statusCode,
+ body: boom.output.payload,
+ });
+ }
+ }
+}
diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts
index b7fa405258e9f..4c31ea7d12321 100644
--- a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts
+++ b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts
@@ -18,6 +18,7 @@ import { CreateNotificationPolicyRoute } from '../routes/notification_policies/c
import { GetNotificationPolicyRoute } from '../routes/notification_policies/get_notification_policy_route';
import { UpdateNotificationPolicyRoute } from '../routes/notification_policies/update_notification_policy_route';
import { DeleteNotificationPolicyRoute } from '../routes/notification_policies/delete_notification_policy_route';
+import { ListNotificationPoliciesRoute } from '../routes/notification_policies/list_notification_policies_route';
export function bindRoutes({ bind }: ContainerModuleLoadOptions) {
bind(Route).toConstantValue(CreateRuleRoute);
@@ -31,4 +32,5 @@ export function bindRoutes({ bind }: ContainerModuleLoadOptions) {
bind(Route).toConstantValue(GetNotificationPolicyRoute);
bind(Route).toConstantValue(UpdateNotificationPolicyRoute);
bind(Route).toConstantValue(DeleteNotificationPolicyRoute);
+ bind(Route).toConstantValue(ListNotificationPoliciesRoute);
}
diff --git a/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json b/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json
index 755b18167506f..5a6d62a1e9acd 100644
--- a/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json
+++ b/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json
@@ -54,9 +54,10 @@
"@kbn/core-user-profile-server",
"@kbn/es-mappings",
"@kbn/std",
+ "@kbn/eval-kql",
+ "@kbn/react-query",
"@kbn/core-security-server",
- "@kbn/encrypted-saved-objects-plugin",
- "@kbn/eval-kql"
+ "@kbn/encrypted-saved-objects-plugin"
],
"exclude": ["target/**/*"]
}
diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/index.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/index.ts
index a9e37dab6b752..907158541e1fb 100644
--- a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/index.ts
+++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/index.ts
@@ -11,6 +11,7 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
describe('notification_policy', () => {
loadTestFile(require.resolve('./create_notification_policy'));
loadTestFile(require.resolve('./get_notification_policy'));
+ loadTestFile(require.resolve('./list_notification_policies'));
loadTestFile(require.resolve('./update_notification_policy'));
loadTestFile(require.resolve('./delete_notification_policy'));
});
diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/list_notification_policies.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/list_notification_policies.ts
new file mode 100644
index 0000000000000..7d01cf342a26b
--- /dev/null
+++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/list_notification_policies.ts
@@ -0,0 +1,118 @@
+/*
+ * 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 expect from '@kbn/expect';
+import type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context';
+import type { RoleCredentials } from '../../../services';
+
+const NOTIFICATION_POLICY_API_PATH = '/internal/alerting/v2/notification_policies';
+const NOTIFICATION_POLICY_SO_TYPE = 'alerting_notification_policy';
+
+export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
+ const samlAuth = getService('samlAuth');
+ const supertestWithoutAuth = getService('supertestWithoutAuth');
+ const kibanaServer = getService('kibanaServer');
+
+ async function createPolicy(roleAuthc: RoleCredentials, name: string) {
+ return supertestWithoutAuth
+ .post(NOTIFICATION_POLICY_API_PATH)
+ .set(roleAuthc.apiKeyHeader)
+ .set(samlAuth.getInternalRequestHeader())
+ .send({
+ name,
+ description: `${name} description`,
+ destinations: [{ type: 'workflow', id: `${name}-workflow-id` }],
+ });
+ }
+
+ describe('List Notification Policies API', function () {
+ let roleAuthc: RoleCredentials;
+
+ before(async () => {
+ await kibanaServer.savedObjects.clean({ types: [NOTIFICATION_POLICY_SO_TYPE] });
+ roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
+ });
+
+ after(async () => {
+ await kibanaServer.savedObjects.clean({ types: [NOTIFICATION_POLICY_SO_TYPE] });
+ await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
+ });
+
+ it('should return empty list when no policies exist', async () => {
+ const response = await supertestWithoutAuth
+ .get(NOTIFICATION_POLICY_API_PATH)
+ .set(roleAuthc.apiKeyHeader)
+ .set(samlAuth.getInternalRequestHeader());
+
+ expect(response.status).to.be(200);
+ expect(response.body.items).to.be.an('array');
+ expect(response.body.items.length).to.be(0);
+ expect(response.body.total).to.be(0);
+ expect(response.body.page).to.be(1);
+ expect(response.body.perPage).to.be(20);
+ });
+
+ it('should return created notification policies', async () => {
+ const createResponse1 = await createPolicy(roleAuthc, 'policy-1');
+ expect(createResponse1.status).to.be(200);
+
+ const createResponse2 = await createPolicy(roleAuthc, 'policy-2');
+ expect(createResponse2.status).to.be(200);
+
+ const response = await supertestWithoutAuth
+ .get(NOTIFICATION_POLICY_API_PATH)
+ .set(roleAuthc.apiKeyHeader)
+ .set(samlAuth.getInternalRequestHeader());
+
+ expect(response.status).to.be(200);
+ expect(response.body.items).to.be.an('array');
+ expect(response.body.items.length).to.be(2);
+ expect(response.body.total).to.be(2);
+
+ const names = response.body.items.map((item: { name: string }) => item.name);
+ expect(names).to.contain('policy-1');
+ expect(names).to.contain('policy-2');
+
+ for (const item of response.body.items) {
+ expect(item.id).to.be.a('string');
+ expect(item.name).to.be.a('string');
+ expect(item.description).to.be.a('string');
+ expect(item.destinations).to.be.an('array');
+ expect(item.createdAt).to.be.a('string');
+ expect(item.updatedAt).to.be.a('string');
+ }
+ });
+
+ it('should paginate results', async () => {
+ await createPolicy(roleAuthc, 'policy-3');
+
+ const firstPage = await supertestWithoutAuth
+ .get(NOTIFICATION_POLICY_API_PATH)
+ .query({ page: 1, perPage: 2 })
+ .set(roleAuthc.apiKeyHeader)
+ .set(samlAuth.getInternalRequestHeader());
+
+ expect(firstPage.status).to.be(200);
+ expect(firstPage.body.items.length).to.be(2);
+ expect(firstPage.body.total).to.be(3);
+ expect(firstPage.body.page).to.be(1);
+ expect(firstPage.body.perPage).to.be(2);
+
+ const secondPage = await supertestWithoutAuth
+ .get(NOTIFICATION_POLICY_API_PATH)
+ .query({ page: 2, perPage: 2 })
+ .set(roleAuthc.apiKeyHeader)
+ .set(samlAuth.getInternalRequestHeader());
+
+ expect(secondPage.status).to.be(200);
+ expect(secondPage.body.items.length).to.be(1);
+ expect(secondPage.body.total).to.be(3);
+ expect(secondPage.body.page).to.be(2);
+ expect(secondPage.body.perPage).to.be(2);
+ });
+ });
+}