diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx
index 57e6fc4a9e18b..7d9a963c9c6b3 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx
@@ -13,12 +13,13 @@ import {
IUiSettingsClient,
ApplicationStart,
} from 'kibana/public';
-import { BASE_PATH, Section } from './constants';
+import { BASE_PATH, Section, routeToAlertDetails } from './constants';
import { TriggersActionsUIHome } from './home';
import { AppContextProvider, useAppDependencies } from './app_context';
import { hasShowAlertsCapability } from './lib/capabilities';
import { LegacyDependencies, ActionTypeModel, AlertTypeModel } from '../types';
import { TypeRegistry } from './type_registry';
+import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route';
export interface AppDeps {
chrome: ChromeStart;
@@ -53,11 +54,8 @@ export const AppWithoutRouter = ({ sectionsRegex }: any) => {
const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors';
return (
-
+
+ {canShowAlerts && }
);
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts
index a8364ffe21019..11b094dea0e62 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts
@@ -13,6 +13,7 @@ export type Section = 'connectors' | 'alerts';
export const routeToHome = `${BASE_PATH}`;
export const routeToConnectors = `${BASE_PATH}/connectors`;
export const routeToAlerts = `${BASE_PATH}/alerts`;
+export const routeToAlertDetails = `${BASE_PATH}/alert/:alertId`;
export { TIME_UNITS } from './time_units';
export enum SORT_ORDERS {
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts
index bc2949917edea..00a55bb2588bb 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts
@@ -24,6 +24,7 @@ describe('loadActionTypes', () => {
{
id: 'test',
name: 'Test',
+ enabled: true,
},
];
http.get.mockResolvedValueOnce(resolvedValue);
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts
index 0106970cf9c38..35d1a095188de 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts
@@ -8,15 +8,22 @@ import { Alert, AlertType } from '../../types';
import { httpServiceMock } from '../../../../../../../../src/core/public/mocks';
import {
createAlert,
+ deleteAlert,
deleteAlerts,
disableAlerts,
enableAlerts,
+ disableAlert,
+ enableAlert,
+ loadAlert,
loadAlerts,
loadAlertTypes,
muteAlerts,
unmuteAlerts,
+ muteAlert,
+ unmuteAlert,
updateAlert,
} from './alert_api';
+import uuid from 'uuid';
const http = httpServiceMock.createStartContract();
@@ -42,6 +49,31 @@ describe('loadAlertTypes', () => {
});
});
+describe('loadAlert', () => {
+ test('should call get API with base parameters', async () => {
+ const alertId = uuid.v4();
+ const resolvedValue = {
+ id: alertId,
+ name: 'name',
+ tags: [],
+ enabled: true,
+ alertTypeId: '.noop',
+ schedule: { interval: '1s' },
+ actions: [],
+ params: {},
+ createdBy: null,
+ updatedBy: null,
+ throttle: null,
+ muteAll: false,
+ mutedInstanceIds: [],
+ };
+ http.get.mockResolvedValueOnce(resolvedValue);
+
+ expect(await loadAlert({ http, alertId })).toEqual(resolvedValue);
+ expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`);
+ });
+});
+
describe('loadAlerts', () => {
test('should call find API with base parameters', async () => {
const resolvedValue = {
@@ -230,6 +262,19 @@ describe('loadAlerts', () => {
});
});
+describe('deleteAlert', () => {
+ test('should call delete API for alert', async () => {
+ const id = '1';
+ const result = await deleteAlert({ http, id });
+ expect(result).toEqual(undefined);
+ expect(http.delete.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "/api/alert/1",
+ ]
+ `);
+ });
+});
+
describe('deleteAlerts', () => {
test('should call delete API for each alert', async () => {
const ids = ['1', '2', '3'];
@@ -335,6 +380,62 @@ describe('updateAlert', () => {
});
});
+describe('enableAlert', () => {
+ test('should call enable alert API', async () => {
+ const result = await enableAlert({ http, id: '1' });
+ expect(result).toEqual(undefined);
+ expect(http.post.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "/api/alert/1/_enable",
+ ],
+ ]
+ `);
+ });
+});
+
+describe('disableAlert', () => {
+ test('should call disable alert API', async () => {
+ const result = await disableAlert({ http, id: '1' });
+ expect(result).toEqual(undefined);
+ expect(http.post.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "/api/alert/1/_disable",
+ ],
+ ]
+ `);
+ });
+});
+
+describe('muteAlert', () => {
+ test('should call mute alert API', async () => {
+ const result = await muteAlert({ http, id: '1' });
+ expect(result).toEqual(undefined);
+ expect(http.post.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "/api/alert/1/_mute_all",
+ ],
+ ]
+ `);
+ });
+});
+
+describe('unmuteAlert', () => {
+ test('should call unmute alert API', async () => {
+ const result = await unmuteAlert({ http, id: '1' });
+ expect(result).toEqual(undefined);
+ expect(http.post.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "/api/alert/1/_unmute_all",
+ ],
+ ]
+ `);
+ });
+});
+
describe('enableAlerts', () => {
test('should call enable alert API per alert', async () => {
const ids = ['1', '2', '3'];
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts
index 0b4f5731c1315..acc318bd5fbea 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts
@@ -12,6 +12,16 @@ export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise {
+ return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`);
+}
+
export async function loadAlerts({
http,
page,
@@ -55,6 +65,10 @@ export async function loadAlerts({
});
}
+export async function deleteAlert({ id, http }: { id: string; http: HttpSetup }): Promise {
+ await http.delete(`${BASE_ALERT_API_PATH}/${id}`);
+}
+
export async function deleteAlerts({
ids,
http,
@@ -62,7 +76,7 @@ export async function deleteAlerts({
ids: string[];
http: HttpSetup;
}): Promise {
- await Promise.all(ids.map(id => http.delete(`${BASE_ALERT_API_PATH}/${id}`)));
+ await Promise.all(ids.map(id => deleteAlert({ http, id })));
}
export async function createAlert({
@@ -91,6 +105,10 @@ export async function updateAlert({
});
}
+export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise {
+ await http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`);
+}
+
export async function enableAlerts({
ids,
http,
@@ -98,7 +116,11 @@ export async function enableAlerts({
ids: string[];
http: HttpSetup;
}): Promise {
- await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`)));
+ await Promise.all(ids.map(id => enableAlert({ id, http })));
+}
+
+export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise {
+ await http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`);
}
export async function disableAlerts({
@@ -108,11 +130,19 @@ export async function disableAlerts({
ids: string[];
http: HttpSetup;
}): Promise {
- await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`)));
+ await Promise.all(ids.map(id => disableAlert({ id, http })));
+}
+
+export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise {
+ await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`);
}
export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise {
- await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`)));
+ await Promise.all(ids.map(id => muteAlert({ http, id })));
+}
+
+export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise {
+ await http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`);
}
export async function unmuteAlerts({
@@ -122,5 +152,5 @@ export async function unmuteAlerts({
ids: string[];
http: HttpSetup;
}): Promise {
- await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`)));
+ await Promise.all(ids.map(id => unmuteAlert({ id, http })));
}
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts
new file mode 100644
index 0000000000000..90f575d9391b3
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { throwIfAbsent, throwIfIsntContained } from './value_validators';
+import uuid from 'uuid';
+
+describe('throwIfAbsent', () => {
+ test('throws if value is absent', () => {
+ [undefined, null].forEach(val => {
+ expect(() => {
+ throwIfAbsent('OMG no value')(val);
+ }).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`);
+ });
+ });
+
+ test('doesnt throws if value is present but falsey', () => {
+ [false, ''].forEach(val => {
+ expect(throwIfAbsent('OMG no value')(val)).toEqual(val);
+ });
+ });
+
+ test('doesnt throw if value is present', () => {
+ expect(throwIfAbsent('OMG no value')({})).toEqual({});
+ });
+});
+
+describe('throwIfIsntContained', () => {
+ test('throws if value is absent', () => {
+ expect(() => {
+ throwIfIsntContained(new Set([uuid.v4()]), 'OMG no value', val => val)([uuid.v4()]);
+ }).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`);
+ });
+
+ test('throws if value is absent using custom message', () => {
+ const id = uuid.v4();
+ expect(() => {
+ throwIfIsntContained(
+ new Set([id]),
+ (value: string) => `OMG no ${value}`,
+ val => val
+ )([uuid.v4()]);
+ }).toThrow(`OMG no ${id}`);
+ });
+
+ test('returns values if value is present', () => {
+ const id = uuid.v4();
+ const values = [uuid.v4(), uuid.v4(), id, uuid.v4()];
+ expect(throwIfIsntContained(new Set([id]), 'OMG no value', val => val)(values)).toEqual(
+ values
+ );
+ });
+
+ test('returns values if multiple values is present', () => {
+ const [firstId, secondId] = [uuid.v4(), uuid.v4()];
+ const values = [uuid.v4(), uuid.v4(), secondId, uuid.v4(), firstId];
+ expect(
+ throwIfIsntContained(new Set([firstId, secondId]), 'OMG no value', val => val)(values)
+ ).toEqual(values);
+ });
+
+ test('allows a custom value extractor', () => {
+ const [firstId, secondId] = [uuid.v4(), uuid.v4()];
+ const values = [
+ { id: firstId, some: 'prop' },
+ { id: secondId, someOther: 'prop' },
+ ];
+ expect(
+ throwIfIsntContained<{ id: string }>(
+ new Set([firstId, secondId]),
+ 'OMG no value',
+ (val: { id: string }) => val.id
+ )(values)
+ ).toEqual(values);
+ });
+});
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts
new file mode 100644
index 0000000000000..7ee7359086406
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { constant } from 'lodash';
+
+export function throwIfAbsent(message: string) {
+ return (value: T | undefined): T => {
+ if (value === undefined || value === null) {
+ throw new Error(message);
+ }
+ return value;
+ };
+}
+
+export function throwIfIsntContained(
+ requiredValues: Set,
+ message: string | ((requiredValue: string) => string),
+ valueExtractor: (value: T) => string
+) {
+ const toError = typeof message === 'function' ? message : constant(message);
+ return (values: T[]) => {
+ const availableValues = new Set(values.map(valueExtractor));
+ for (const value of requiredValues.values()) {
+ if (!availableValues.has(value)) {
+ throw new Error(toError(value));
+ }
+ }
+ return values;
+ };
+}
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx
index 6896ac954bb06..f27f7d8c3054d 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx
@@ -82,7 +82,11 @@ describe('action_connector_form', () => {
editFlyoutVisible: false,
setEditFlyoutVisibility: () => {},
actionTypesIndex: {
- 'my-action-type': { id: 'my-action-type', name: 'my-action-type-name' },
+ 'my-action-type': {
+ id: 'my-action-type',
+ name: 'my-action-type-name',
+ enabled: true,
+ },
},
reloadConnectors: () => {
return new Promise(() => {});
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx
index 6ef2f62315d9a..6d98a5e3d120f 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx
@@ -75,8 +75,8 @@ describe('connector_add_flyout', () => {
editFlyoutVisible: false,
setEditFlyoutVisibility: state => {},
actionTypesIndex: {
- 'first-action-type': { id: 'first-action-type', name: 'first' },
- 'second-action-type': { id: 'second-action-type', name: 'second' },
+ 'first-action-type': { id: 'first-action-type', name: 'first', enabled: true },
+ 'second-action-type': { id: 'second-action-type', name: 'second', enabled: true },
},
reloadConnectors: () => {
return new Promise(() => {});
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx
index 71ba52f047d61..a03296c7c3679 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx
@@ -58,7 +58,9 @@ describe('connector_add_flyout', () => {
setAddFlyoutVisibility: state => {},
editFlyoutVisible: false,
setEditFlyoutVisibility: state => {},
- actionTypesIndex: { 'my-action-type': { id: 'my-action-type', name: 'test' } },
+ actionTypesIndex: {
+ 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true },
+ },
reloadConnectors: () => {
return new Promise(() => {});
},
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx
index 57e950a98eb2a..0dc38523bfab8 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx
@@ -84,7 +84,7 @@ describe('connector_edit_flyout', () => {
editFlyoutVisible: true,
setEditFlyoutVisibility: state => {},
actionTypesIndex: {
- 'test-action-type-id': { id: 'test-action-type-id', name: 'test' },
+ 'test-action-type-id': { id: 'test-action-type-id', name: 'test', enabled: true },
},
reloadConnectors: () => {
return new Promise(() => {});
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx
new file mode 100644
index 0000000000000..228bceb87cad7
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx
@@ -0,0 +1,519 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import * as React from 'react';
+import uuid from 'uuid';
+import { shallow } from 'enzyme';
+import { AlertDetails } from './alert_details';
+import { Alert, ActionType } from '../../../../types';
+import { EuiTitle, EuiBadge, EuiFlexItem, EuiButtonEmpty, EuiSwitch } from '@elastic/eui';
+import { times, random } from 'lodash';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+jest.mock('../../../app_context', () => ({
+ useAppDependencies: jest.fn(() => ({
+ http: jest.fn(),
+ legacy: {
+ capabilities: {
+ get: jest.fn(() => ({})),
+ },
+ },
+ })),
+}));
+
+jest.mock('../../../lib/capabilities', () => ({
+ hasSaveAlertsCapability: jest.fn(() => true),
+}));
+
+const mockAlertApis = {
+ muteAlert: jest.fn(),
+ unmuteAlert: jest.fn(),
+ enableAlert: jest.fn(),
+ disableAlert: jest.fn(),
+};
+
+// const AlertDetails = withBulkAlertOperations(RawAlertDetails);
+describe('alert_details', () => {
+ // mock Api handlers
+
+ it('renders the alert name as a title', () => {
+ const alert = mockAlert();
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ expect(
+ shallow(
+
+ ).containsMatchingElement(
+
+ {alert.name}
+
+ )
+ ).toBeTruthy();
+ });
+
+ it('renders the alert type badge', () => {
+ const alert = mockAlert();
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ expect(
+ shallow(
+
+ ).containsMatchingElement({alertType.name})
+ ).toBeTruthy();
+ });
+
+ describe('actions', () => {
+ it('renders an alert action', () => {
+ const alert = mockAlert({
+ actions: [
+ {
+ group: 'default',
+ id: uuid.v4(),
+ params: {},
+ actionTypeId: '.server-log',
+ },
+ ],
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ const actionTypes: ActionType[] = [
+ {
+ id: '.server-log',
+ name: 'Server log',
+ enabled: true,
+ },
+ ];
+
+ expect(
+ shallow(
+
+ ).containsMatchingElement(
+
+ {actionTypes[0].name}
+
+ )
+ ).toBeTruthy();
+ });
+
+ it('renders a counter for multiple alert action', () => {
+ const actionCount = random(1, 10);
+ const alert = mockAlert({
+ actions: [
+ {
+ group: 'default',
+ id: uuid.v4(),
+ params: {},
+ actionTypeId: '.server-log',
+ },
+ ...times(actionCount, () => ({
+ group: 'default',
+ id: uuid.v4(),
+ params: {},
+ actionTypeId: '.email',
+ })),
+ ],
+ });
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+ const actionTypes: ActionType[] = [
+ {
+ id: '.server-log',
+ name: 'Server log',
+ enabled: true,
+ },
+ {
+ id: '.email',
+ name: 'Send email',
+ enabled: true,
+ },
+ ];
+
+ const details = shallow(
+
+ );
+
+ expect(
+ details.containsMatchingElement(
+
+ {actionTypes[0].name}
+
+ )
+ ).toBeTruthy();
+
+ expect(
+ details.containsMatchingElement(
+
+ {`+${actionCount}`}
+
+ )
+ ).toBeTruthy();
+ });
+ });
+
+ describe('links', () => {
+ it('links to the Edit flyout', () => {
+ const alert = mockAlert();
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ expect(
+ shallow(
+
+ ).containsMatchingElement(
+
+
+
+ )
+ ).toBeTruthy();
+ });
+
+ it('links to the app that created the alert', () => {
+ const alert = mockAlert();
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ expect(
+ shallow(
+
+ ).containsMatchingElement(
+
+
+
+ )
+ ).toBeTruthy();
+ });
+
+ it('links to the activity log', () => {
+ const alert = mockAlert();
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ expect(
+ shallow(
+
+ ).containsMatchingElement(
+
+
+
+ )
+ ).toBeTruthy();
+ });
+ });
+});
+
+describe('enable button', () => {
+ it('should render an enable button when alert is enabled', () => {
+ const alert = mockAlert({
+ enabled: true,
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ const enableButton = shallow(
+
+ )
+ .find(EuiSwitch)
+ .find('[name="enable"]')
+ .first();
+
+ expect(enableButton.props()).toMatchObject({
+ checked: true,
+ disabled: false,
+ });
+ });
+
+ it('should render an enable button when alert is disabled', () => {
+ const alert = mockAlert({
+ enabled: false,
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ const enableButton = shallow(
+
+ )
+ .find(EuiSwitch)
+ .find('[name="enable"]')
+ .first();
+
+ expect(enableButton.props()).toMatchObject({
+ checked: false,
+ disabled: false,
+ });
+ });
+
+ it('should enable the alert when alert is disabled and button is clicked', () => {
+ const alert = mockAlert({
+ enabled: true,
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ const disableAlert = jest.fn();
+ const enableButton = shallow(
+
+ )
+ .find(EuiSwitch)
+ .find('[name="enable"]')
+ .first();
+
+ enableButton.simulate('click');
+ const handler = enableButton.prop('onChange');
+ expect(typeof handler).toEqual('function');
+ expect(disableAlert).toHaveBeenCalledTimes(0);
+ handler!({} as React.FormEvent);
+ expect(disableAlert).toHaveBeenCalledTimes(1);
+ });
+
+ it('should disable the alert when alert is enabled and button is clicked', () => {
+ const alert = mockAlert({
+ enabled: false,
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ const enableAlert = jest.fn();
+ const enableButton = shallow(
+
+ )
+ .find(EuiSwitch)
+ .find('[name="enable"]')
+ .first();
+
+ enableButton.simulate('click');
+ const handler = enableButton.prop('onChange');
+ expect(typeof handler).toEqual('function');
+ expect(enableAlert).toHaveBeenCalledTimes(0);
+ handler!({} as React.FormEvent);
+ expect(enableAlert).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe('mute button', () => {
+ it('should render an mute button when alert is enabled', () => {
+ const alert = mockAlert({
+ enabled: true,
+ muteAll: false,
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ const enableButton = shallow(
+
+ )
+ .find(EuiSwitch)
+ .find('[name="mute"]')
+ .first();
+
+ expect(enableButton.props()).toMatchObject({
+ checked: false,
+ disabled: false,
+ });
+ });
+
+ it('should render an muted button when alert is muted', () => {
+ const alert = mockAlert({
+ enabled: true,
+ muteAll: true,
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ const enableButton = shallow(
+
+ )
+ .find(EuiSwitch)
+ .find('[name="mute"]')
+ .first();
+
+ expect(enableButton.props()).toMatchObject({
+ checked: true,
+ disabled: false,
+ });
+ });
+
+ it('should mute the alert when alert is unmuted and button is clicked', () => {
+ const alert = mockAlert({
+ enabled: true,
+ muteAll: false,
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ const muteAlert = jest.fn();
+ const enableButton = shallow(
+
+ )
+ .find(EuiSwitch)
+ .find('[name="mute"]')
+ .first();
+
+ enableButton.simulate('click');
+ const handler = enableButton.prop('onChange');
+ expect(typeof handler).toEqual('function');
+ expect(muteAlert).toHaveBeenCalledTimes(0);
+ handler!({} as React.FormEvent);
+ expect(muteAlert).toHaveBeenCalledTimes(1);
+ });
+
+ it('should unmute the alert when alert is muted and button is clicked', () => {
+ const alert = mockAlert({
+ enabled: true,
+ muteAll: true,
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ const unmuteAlert = jest.fn();
+ const enableButton = shallow(
+
+ )
+ .find(EuiSwitch)
+ .find('[name="mute"]')
+ .first();
+
+ enableButton.simulate('click');
+ const handler = enableButton.prop('onChange');
+ expect(typeof handler).toEqual('function');
+ expect(unmuteAlert).toHaveBeenCalledTimes(0);
+ handler!({} as React.FormEvent);
+ expect(unmuteAlert).toHaveBeenCalledTimes(1);
+ });
+
+ it('should disabled mute button when alert is disabled', () => {
+ const alert = mockAlert({
+ enabled: false,
+ muteAll: false,
+ });
+
+ const alertType = {
+ id: '.noop',
+ name: 'No Op',
+ };
+
+ const enableButton = shallow(
+
+ )
+ .find(EuiSwitch)
+ .find('[name="mute"]')
+ .first();
+
+ expect(enableButton.props()).toMatchObject({
+ checked: false,
+ disabled: true,
+ });
+ });
+});
+
+function mockAlert(overloads: Partial = {}): Alert {
+ return {
+ id: uuid.v4(),
+ enabled: true,
+ name: `alert-${uuid.v4()}`,
+ tags: [],
+ alertTypeId: '.noop',
+ consumer: 'consumer',
+ schedule: { interval: '1m' },
+ actions: [],
+ params: {},
+ createdBy: null,
+ updatedBy: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ apiKeyOwner: null,
+ throttle: null,
+ muteAll: false,
+ mutedInstanceIds: [],
+ ...overloads,
+ };
+}
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx
new file mode 100644
index 0000000000000..ffdf846efd49d
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx
@@ -0,0 +1,176 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+import { indexBy } from 'lodash';
+import {
+ EuiPageBody,
+ EuiPageContent,
+ EuiPageContentHeader,
+ EuiPageContentHeaderSection,
+ EuiTitle,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiBadge,
+ EuiPage,
+ EuiPageContentBody,
+ EuiButtonEmpty,
+ EuiSwitch,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { useAppDependencies } from '../../../app_context';
+import { hasSaveAlertsCapability } from '../../../lib/capabilities';
+import { Alert, AlertType, ActionType } from '../../../../types';
+import {
+ ComponentOpts as BulkOperationsComponentOpts,
+ withBulkAlertOperations,
+} from '../../common/components/with_bulk_alert_api_operations';
+
+type AlertDetailsProps = {
+ alert: Alert;
+ alertType: AlertType;
+ actionTypes: ActionType[];
+} & Pick;
+
+export const AlertDetails: React.FunctionComponent = ({
+ alert,
+ alertType,
+ actionTypes,
+ disableAlert,
+ enableAlert,
+ unmuteAlert,
+ muteAlert,
+}) => {
+ const { capabilities } = useAppDependencies();
+
+ const canSave = hasSaveAlertsCapability(capabilities);
+
+ const actionTypesByTypeId = indexBy(actionTypes, 'id');
+ const [firstAction, ...otherActions] = alert.actions;
+
+ const [isEnabled, setIsEnabled] = useState(alert.enabled);
+ const [isMuted, setIsMuted] = useState(alert.muteAll);
+
+ return (
+
+
+
+
+
+
+ {alert.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {alertType.name}
+
+ {firstAction && (
+
+
+ {actionTypesByTypeId[firstAction.actionTypeId].name ??
+ firstAction.actionTypeId}
+
+
+ )}
+ {otherActions.length ? (
+
+ +{otherActions.length}
+
+ ) : null}
+
+
+
+
+
+ {
+ if (isEnabled) {
+ setIsEnabled(false);
+ await disableAlert(alert);
+ } else {
+ setIsEnabled(true);
+ await enableAlert(alert);
+ }
+ }}
+ label={
+
+ }
+ />
+
+
+ {
+ if (isMuted) {
+ setIsMuted(false);
+ await unmuteAlert(alert);
+ } else {
+ setIsMuted(true);
+ await muteAlert(alert);
+ }
+ }}
+ label={
+
+ }
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+export const AlertDetailsWithApi = withBulkAlertOperations(AlertDetails);
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx
new file mode 100644
index 0000000000000..7a40104e97d9f
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx
@@ -0,0 +1,409 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import * as React from 'react';
+import uuid from 'uuid';
+import { shallow } from 'enzyme';
+import { createMemoryHistory, createLocation } from 'history';
+import { ToastsApi } from 'kibana/public';
+import { AlertDetailsRoute, getAlertData } from './alert_details_route';
+import { Alert } from '../../../../types';
+import { EuiLoadingSpinner } from '@elastic/eui';
+
+jest.mock('../../../app_context', () => {
+ const toastNotifications = jest.fn();
+ return {
+ useAppDependencies: jest.fn(() => ({ toastNotifications })),
+ };
+});
+describe('alert_details_route', () => {
+ it('render a loader while fetching data', () => {
+ const alert = mockAlert();
+
+ expect(
+ shallow(
+
+ ).containsMatchingElement()
+ ).toBeTruthy();
+ });
+});
+
+describe('getAlertData useEffect handler', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('fetches alert', async () => {
+ const alert = mockAlert();
+ const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
+ const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
+
+ loadAlert.mockImplementationOnce(async () => alert);
+
+ const toastNotifications = ({
+ addDanger: jest.fn(),
+ } as unknown) as ToastsApi;
+
+ await getAlertData(
+ alert.id,
+ loadAlert,
+ loadAlertTypes,
+ loadActionTypes,
+ setAlert,
+ setAlertType,
+ setActionTypes,
+ toastNotifications
+ );
+
+ expect(loadAlert).toHaveBeenCalledWith(alert.id);
+ expect(setAlert).toHaveBeenCalledWith(alert);
+ });
+
+ it('fetches alert and action types', async () => {
+ const actionType = {
+ id: '.server-log',
+ name: 'Server log',
+ enabled: true,
+ };
+ const alert = mockAlert({
+ actions: [
+ {
+ group: '',
+ id: uuid.v4(),
+ actionTypeId: actionType.id,
+ params: {},
+ },
+ ],
+ });
+ const alertType = {
+ id: alert.alertTypeId,
+ name: 'type name',
+ };
+ const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
+ const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
+
+ loadAlert.mockImplementation(async () => alert);
+ loadAlertTypes.mockImplementation(async () => [alertType]);
+ loadActionTypes.mockImplementation(async () => [actionType]);
+
+ const toastNotifications = ({
+ addDanger: jest.fn(),
+ } as unknown) as ToastsApi;
+
+ await getAlertData(
+ alert.id,
+ loadAlert,
+ loadAlertTypes,
+ loadActionTypes,
+ setAlert,
+ setAlertType,
+ setActionTypes,
+ toastNotifications
+ );
+
+ expect(loadAlertTypes).toHaveBeenCalledTimes(1);
+ expect(loadActionTypes).toHaveBeenCalledTimes(1);
+
+ expect(setAlertType).toHaveBeenCalledWith(alertType);
+ expect(setActionTypes).toHaveBeenCalledWith([actionType]);
+ });
+
+ it('displays an error if the alert isnt found', async () => {
+ const actionType = {
+ id: '.server-log',
+ name: 'Server log',
+ enabled: true,
+ };
+ const alert = mockAlert({
+ actions: [
+ {
+ group: '',
+ id: uuid.v4(),
+ actionTypeId: actionType.id,
+ params: {},
+ },
+ ],
+ });
+
+ const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
+ const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
+
+ loadAlert.mockImplementation(async () => {
+ throw new Error('OMG');
+ });
+
+ const toastNotifications = ({
+ addDanger: jest.fn(),
+ } as unknown) as ToastsApi;
+ await getAlertData(
+ alert.id,
+ loadAlert,
+ loadAlertTypes,
+ loadActionTypes,
+ setAlert,
+ setAlertType,
+ setActionTypes,
+ toastNotifications
+ );
+ expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1);
+ expect(toastNotifications.addDanger).toHaveBeenCalledWith({
+ title: 'Unable to load alert: OMG',
+ });
+ });
+
+ it('displays an error if the alert type isnt loaded', async () => {
+ const actionType = {
+ id: '.server-log',
+ name: 'Server log',
+ enabled: true,
+ };
+ const alert = mockAlert({
+ actions: [
+ {
+ group: '',
+ id: uuid.v4(),
+ actionTypeId: actionType.id,
+ params: {},
+ },
+ ],
+ });
+
+ const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
+ const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
+
+ loadAlert.mockImplementation(async () => alert);
+
+ loadAlertTypes.mockImplementation(async () => {
+ throw new Error('OMG no alert type');
+ });
+ loadActionTypes.mockImplementation(async () => [actionType]);
+
+ const toastNotifications = ({
+ addDanger: jest.fn(),
+ } as unknown) as ToastsApi;
+ await getAlertData(
+ alert.id,
+ loadAlert,
+ loadAlertTypes,
+ loadActionTypes,
+ setAlert,
+ setAlertType,
+ setActionTypes,
+ toastNotifications
+ );
+ expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1);
+ expect(toastNotifications.addDanger).toHaveBeenCalledWith({
+ title: 'Unable to load alert: OMG no alert type',
+ });
+ });
+
+ it('displays an error if the action type isnt loaded', async () => {
+ const actionType = {
+ id: '.server-log',
+ name: 'Server log',
+ enabled: true,
+ };
+ const alert = mockAlert({
+ actions: [
+ {
+ group: '',
+ id: uuid.v4(),
+ actionTypeId: actionType.id,
+ params: {},
+ },
+ ],
+ });
+ const alertType = {
+ id: alert.alertTypeId,
+ name: 'type name',
+ };
+
+ const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
+ const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
+
+ loadAlert.mockImplementation(async () => alert);
+
+ loadAlertTypes.mockImplementation(async () => [alertType]);
+ loadActionTypes.mockImplementation(async () => {
+ throw new Error('OMG no action type');
+ });
+
+ const toastNotifications = ({
+ addDanger: jest.fn(),
+ } as unknown) as ToastsApi;
+ await getAlertData(
+ alert.id,
+ loadAlert,
+ loadAlertTypes,
+ loadActionTypes,
+ setAlert,
+ setAlertType,
+ setActionTypes,
+ toastNotifications
+ );
+ expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1);
+ expect(toastNotifications.addDanger).toHaveBeenCalledWith({
+ title: 'Unable to load alert: OMG no action type',
+ });
+ });
+
+ it('displays an error if the alert type isnt found', async () => {
+ const actionType = {
+ id: '.server-log',
+ name: 'Server log',
+ enabled: true,
+ };
+ const alert = mockAlert({
+ actions: [
+ {
+ group: '',
+ id: uuid.v4(),
+ actionTypeId: actionType.id,
+ params: {},
+ },
+ ],
+ });
+
+ const alertType = {
+ id: uuid.v4(),
+ name: 'type name',
+ };
+
+ const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
+ const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
+
+ loadAlert.mockImplementation(async () => alert);
+ loadAlertTypes.mockImplementation(async () => [alertType]);
+ loadActionTypes.mockImplementation(async () => [actionType]);
+
+ const toastNotifications = ({
+ addDanger: jest.fn(),
+ } as unknown) as ToastsApi;
+ await getAlertData(
+ alert.id,
+ loadAlert,
+ loadAlertTypes,
+ loadActionTypes,
+ setAlert,
+ setAlertType,
+ setActionTypes,
+ toastNotifications
+ );
+ expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1);
+ expect(toastNotifications.addDanger).toHaveBeenCalledWith({
+ title: `Unable to load alert: Invalid Alert Type: ${alert.alertTypeId}`,
+ });
+ });
+
+ it('displays an error if an action type isnt found', async () => {
+ const availableActionType = {
+ id: '.server-log',
+ name: 'Server log',
+ enabled: true,
+ };
+ const missingActionType = {
+ id: '.noop',
+ name: 'No Op',
+ enabled: true,
+ };
+ const alert = mockAlert({
+ actions: [
+ {
+ group: '',
+ id: uuid.v4(),
+ actionTypeId: availableActionType.id,
+ params: {},
+ },
+ {
+ group: '',
+ id: uuid.v4(),
+ actionTypeId: missingActionType.id,
+ params: {},
+ },
+ ],
+ });
+
+ const alertType = {
+ id: uuid.v4(),
+ name: 'type name',
+ };
+
+ const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
+ const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
+
+ loadAlert.mockImplementation(async () => alert);
+ loadAlertTypes.mockImplementation(async () => [alertType]);
+ loadActionTypes.mockImplementation(async () => [availableActionType]);
+
+ const toastNotifications = ({
+ addDanger: jest.fn(),
+ } as unknown) as ToastsApi;
+ await getAlertData(
+ alert.id,
+ loadAlert,
+ loadAlertTypes,
+ loadActionTypes,
+ setAlert,
+ setAlertType,
+ setActionTypes,
+ toastNotifications
+ );
+ expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1);
+ expect(toastNotifications.addDanger).toHaveBeenCalledWith({
+ title: `Unable to load alert: Invalid Action Type: ${missingActionType.id}`,
+ });
+ });
+});
+
+function mockApis() {
+ return {
+ loadAlert: jest.fn(),
+ loadAlertTypes: jest.fn(),
+ loadActionTypes: jest.fn(),
+ };
+}
+
+function mockStateSetter() {
+ return {
+ setAlert: jest.fn(),
+ setAlertType: jest.fn(),
+ setActionTypes: jest.fn(),
+ };
+}
+
+function mockRouterProps(alert: Alert) {
+ return {
+ match: {
+ isExact: false,
+ path: `/alert/${alert.id}`,
+ url: '',
+ params: { alertId: alert.id },
+ },
+ history: createMemoryHistory(),
+ location: createLocation(`/alert/${alert.id}`),
+ };
+}
+function mockAlert(overloads: Partial = {}): Alert {
+ return {
+ id: uuid.v4(),
+ enabled: true,
+ name: `alert-${uuid.v4()}`,
+ tags: [],
+ alertTypeId: '.noop',
+ consumer: 'consumer',
+ schedule: { interval: '1m' },
+ actions: [],
+ params: {},
+ createdBy: null,
+ updatedBy: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ apiKeyOwner: null,
+ throttle: null,
+ muteAll: false,
+ mutedInstanceIds: [],
+ ...overloads,
+ };
+}
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx
new file mode 100644
index 0000000000000..4e00ea304d987
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import React, { useState, useEffect } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import { EuiLoadingSpinner } from '@elastic/eui';
+import { ToastsApi } from 'kibana/public';
+import { Alert, AlertType, ActionType } from '../../../../types';
+import { useAppDependencies } from '../../../app_context';
+import { AlertDetailsWithApi as AlertDetails } from './alert_details';
+import { throwIfAbsent, throwIfIsntContained } from '../../../lib/value_validators';
+import {
+ ComponentOpts as AlertApis,
+ withBulkAlertOperations,
+} from '../../common/components/with_bulk_alert_api_operations';
+import {
+ ComponentOpts as ActionApis,
+ withActionOperations,
+} from '../../common/components/with_actions_api_operations';
+
+type AlertDetailsRouteProps = RouteComponentProps<{
+ alertId: string;
+}> &
+ Pick &
+ Pick;
+
+export const AlertDetailsRoute: React.FunctionComponent = ({
+ match: {
+ params: { alertId },
+ },
+ loadAlert,
+ loadAlertTypes,
+ loadActionTypes,
+}) => {
+ const { http, toastNotifications } = useAppDependencies();
+
+ const [alert, setAlert] = useState(null);
+ const [alertType, setAlertType] = useState(null);
+ const [actionTypes, setActionTypes] = useState(null);
+
+ useEffect(() => {
+ getAlertData(
+ alertId,
+ loadAlert,
+ loadAlertTypes,
+ loadActionTypes,
+ setAlert,
+ setAlertType,
+ setActionTypes,
+ toastNotifications
+ );
+ }, [alertId, http, loadActionTypes, loadAlert, loadAlertTypes, toastNotifications]);
+
+ return alert && alertType && actionTypes ? (
+
+ ) : (
+
+
+
+ );
+};
+
+export async function getAlertData(
+ alertId: string,
+ loadAlert: AlertApis['loadAlert'],
+ loadAlertTypes: AlertApis['loadAlertTypes'],
+ loadActionTypes: ActionApis['loadActionTypes'],
+ setAlert: React.Dispatch>,
+ setAlertType: React.Dispatch>,
+ setActionTypes: React.Dispatch>,
+ toastNotifications: Pick
+) {
+ try {
+ const loadedAlert = await loadAlert(alertId);
+ setAlert(loadedAlert);
+
+ const [loadedAlertType, loadedActionTypes] = await Promise.all([
+ loadAlertTypes()
+ .then(types => types.find(type => type.id === loadedAlert.alertTypeId))
+ .then(throwIfAbsent(`Invalid Alert Type: ${loadedAlert.alertTypeId}`)),
+ loadActionTypes().then(
+ throwIfIsntContained(
+ new Set(loadedAlert.actions.map(action => action.actionTypeId)),
+ (requiredActionType: string) => `Invalid Action Type: ${requiredActionType}`,
+ (action: ActionType) => action.id
+ )
+ ),
+ ]);
+
+ setAlertType(loadedAlertType);
+ setActionTypes(loadedActionTypes);
+ } catch (e) {
+ toastNotifications.addDanger({
+ title: i18n.translate(
+ 'xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage',
+ {
+ defaultMessage: 'Unable to load alert: {message}',
+ values: {
+ message: e.message,
+ },
+ }
+ ),
+ });
+ }
+}
+
+export const AlertDetailsRouteWithApi = withActionOperations(
+ withBulkAlertOperations(AlertDetailsRoute)
+);
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx
index ff1510ea873d3..f410fff44172f 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx
@@ -21,7 +21,14 @@ jest.mock('../../../lib/alert_api', () => ({
loadAlerts: jest.fn(),
loadAlertTypes: jest.fn(),
}));
-
+jest.mock('react-router-dom', () => ({
+ useHistory: () => ({
+ push: jest.fn(),
+ }),
+ useLocation: () => ({
+ pathname: '/triggersActions/alerts/',
+ }),
+}));
const actionTypeRegistry = actionTypeRegistryMock.create();
const alertTypeRegistry = alertTypeRegistryMock.create();
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx
index 12122983161bd..32de924f63e80 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx
@@ -15,19 +15,23 @@ import {
EuiFlexItem,
EuiIcon,
EuiSpacer,
+ EuiLink,
} from '@elastic/eui';
+import { useHistory } from 'react-router-dom';
import { AlertsContextProvider } from '../../../context/alerts_context';
import { useAppDependencies } from '../../../app_context';
import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types';
import { AlertAdd } from '../../alert_add';
-import { BulkActionPopover } from './bulk_action_popover';
-import { CollapsedItemActions } from './collapsed_item_actions';
+import { BulkOperationPopover } from '../../common/components/bulk_operation_popover';
+import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons';
+import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions';
import { TypeFilter } from './type_filter';
import { ActionTypeFilter } from './action_type_filter';
import { loadAlerts, loadAlertTypes } from '../../../lib/alert_api';
import { loadActionTypes } from '../../../lib/action_connector_api';
import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities';
+import { routeToAlertDetails } from '../../../constants';
const ENTER_KEY = 13;
@@ -43,6 +47,7 @@ interface AlertState {
}
export const AlertsList: React.FunctionComponent = () => {
+ const history = useHistory();
const { http, injectedMetadata, toastNotifications, capabilities } = useAppDependencies();
const canDelete = hasDeleteAlertsCapability(capabilities);
const canSave = hasSaveAlertsCapability(capabilities);
@@ -151,6 +156,18 @@ export const AlertsList: React.FunctionComponent = () => {
sortable: false,
truncateText: true,
'data-test-subj': 'alertsTableCell-name',
+ render: (name: string, alert: AlertTableItem) => {
+ return (
+ {
+ history.push(routeToAlertDetails.replace(`:alertId`, alert.id));
+ }}
+ >
+ {name}
+
+ );
+ },
},
{
field: 'tagsText',
@@ -236,17 +253,19 @@ export const AlertsList: React.FunctionComponent = () => {
{selectedIds.length > 0 && canDelete && (
- setIsPerformingAction(true)}
- onActionPerformed={() => {
- loadAlertsData();
- setIsPerformingAction(false);
- }}
- />
+
+ setIsPerformingAction(true)}
+ onActionPerformed={() => {
+ loadAlertsData();
+ setIsPerformingAction(false);
+ }}
+ />
+
)}
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx
index aa1c6dd7c5b9a..2bac159ed79ed 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx
@@ -20,23 +20,25 @@ import { AlertTableItem } from '../../../../types';
import { useAppDependencies } from '../../../app_context';
import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities';
import {
- deleteAlerts,
- disableAlerts,
- enableAlerts,
- muteAlerts,
- unmuteAlerts,
-} from '../../../lib/alert_api';
+ ComponentOpts as BulkOperationsComponentOpts,
+ withBulkAlertOperations,
+} from '../../common/components/with_bulk_alert_api_operations';
-export interface ComponentOpts {
+export type ComponentOpts = {
item: AlertTableItem;
onAlertChanged: () => void;
-}
+} & BulkOperationsComponentOpts;
export const CollapsedItemActions: React.FunctionComponent = ({
item,
onAlertChanged,
+ disableAlert,
+ enableAlert,
+ unmuteAlert,
+ muteAlert,
+ deleteAlert,
}: ComponentOpts) => {
- const { http, capabilities } = useAppDependencies();
+ const { capabilities } = useAppDependencies();
const canDelete = hasDeleteAlertsCapability(capabilities);
const canSave = hasSaveAlertsCapability(capabilities);
@@ -71,9 +73,9 @@ export const CollapsedItemActions: React.FunctionComponent = ({
data-test-subj="enableSwitch"
onChange={async () => {
if (item.enabled) {
- await disableAlerts({ http, ids: [item.id] });
+ await disableAlert(item);
} else {
- await enableAlerts({ http, ids: [item.id] });
+ await enableAlert(item);
}
onAlertChanged();
}}
@@ -93,9 +95,9 @@ export const CollapsedItemActions: React.FunctionComponent = ({
data-test-subj="muteSwitch"
onChange={async () => {
if (item.muteAll) {
- await unmuteAlerts({ http, ids: [item.id] });
+ await unmuteAlert(item);
} else {
- await muteAlerts({ http, ids: [item.id] });
+ await muteAlert(item);
}
onAlertChanged();
}}
@@ -115,7 +117,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({
color="text"
data-test-subj="deleteAlert"
onClick={async () => {
- await deleteAlerts({ http, ids: [item.id] });
+ await deleteAlert(item);
onAlertChanged();
}}
>
@@ -129,3 +131,5 @@ export const CollapsedItemActions: React.FunctionComponent = ({
);
};
+
+export const CollapsedItemActionsWithApi = withBulkAlertOperations(CollapsedItemActions);
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx
similarity index 52%
rename from x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx
rename to x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx
index 59ec52ac83a6c..9635e6cd11983 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx
@@ -5,34 +5,35 @@
*/
import { i18n } from '@kbn/i18n';
-import React, { useState } from 'react';
+import React, { useState, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiButton, EuiButtonEmpty, EuiFormRow, EuiPopover } from '@elastic/eui';
+import { EuiButtonEmpty } from '@elastic/eui';
-import { AlertTableItem } from '../../../../types';
+import { Alert } from '../../../../types';
import { useAppDependencies } from '../../../app_context';
import {
- deleteAlerts,
- disableAlerts,
- enableAlerts,
- muteAlerts,
- unmuteAlerts,
-} from '../../../lib/alert_api';
+ withBulkAlertOperations,
+ ComponentOpts as BulkOperationsComponentOpts,
+} from './with_bulk_alert_api_operations';
-export interface ComponentOpts {
- selectedItems: AlertTableItem[];
- onPerformingAction: () => void;
- onActionPerformed: () => void;
-}
+export type ComponentOpts = {
+ selectedItems: Alert[];
+ onPerformingAction?: () => void;
+ onActionPerformed?: () => void;
+} & BulkOperationsComponentOpts;
-export const BulkActionPopover: React.FunctionComponent = ({
+export const AlertQuickEditButtons: React.FunctionComponent = ({
selectedItems,
- onPerformingAction,
- onActionPerformed,
+ onPerformingAction = noop,
+ onActionPerformed = noop,
+ muteAlerts,
+ unmuteAlerts,
+ enableAlerts,
+ disableAlerts,
+ deleteAlerts,
}: ComponentOpts) => {
- const { http, toastNotifications } = useAppDependencies();
+ const { toastNotifications } = useAppDependencies();
- const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isMutingAlerts, setIsMutingAlerts] = useState(false);
const [isUnmutingAlerts, setIsUnmutingAlerts] = useState(false);
const [isEnablingAlerts, setIsEnablingAlerts] = useState(false);
@@ -47,9 +48,8 @@ export const BulkActionPopover: React.FunctionComponent = ({
async function onmMuteAllClick() {
onPerformingAction();
setIsMutingAlerts(true);
- const ids = selectedItems.filter(item => !isAlertMuted(item)).map(item => item.id);
try {
- await muteAlerts({ http, ids });
+ await muteAlerts(selectedItems);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(
@@ -68,9 +68,8 @@ export const BulkActionPopover: React.FunctionComponent = ({
async function onUnmuteAllClick() {
onPerformingAction();
setIsUnmutingAlerts(true);
- const ids = selectedItems.filter(isAlertMuted).map(item => item.id);
try {
- await unmuteAlerts({ http, ids });
+ await unmuteAlerts(selectedItems);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(
@@ -89,9 +88,8 @@ export const BulkActionPopover: React.FunctionComponent = ({
async function onEnableAllClick() {
onPerformingAction();
setIsEnablingAlerts(true);
- const ids = selectedItems.filter(isAlertDisabled).map(item => item.id);
try {
- await enableAlerts({ http, ids });
+ await enableAlerts(selectedItems);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(
@@ -110,9 +108,8 @@ export const BulkActionPopover: React.FunctionComponent = ({
async function onDisableAllClick() {
onPerformingAction();
setIsDisablingAlerts(true);
- const ids = selectedItems.filter(item => !isAlertDisabled(item)).map(item => item.id);
try {
- await disableAlerts({ http, ids });
+ await disableAlerts(selectedItems);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(
@@ -131,9 +128,8 @@ export const BulkActionPopover: React.FunctionComponent = ({
async function deleteSelectedItems() {
onPerformingAction();
setIsDeletingAlerts(true);
- const ids = selectedItems.map(item => item.id);
try {
- await deleteAlerts({ http, ids });
+ await deleteAlerts(selectedItems);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(
@@ -150,104 +146,83 @@ export const BulkActionPopover: React.FunctionComponent = ({
}
return (
- setIsPopoverOpen(false)}
- data-test-subj="bulkAction"
- button={
- setIsPopoverOpen(!isPopoverOpen)}
+
+ {!allAlertsMuted && (
+
-
- }
- >
- {!allAlertsMuted && (
-
-
-
-
-
+
)}
{allAlertsMuted && (
-
-
-
-
-
+
+
+
)}
{allAlertsDisabled && (
-
-
-
-
-
+
+
+
)}
{!allAlertsDisabled && (
-
-
-
-
-
- )}
-
-
-
+ )}
+
+
+
+
+
);
};
-function isAlertDisabled(alert: AlertTableItem) {
+export const AlertQuickEditButtonsWithApi = withBulkAlertOperations(AlertQuickEditButtons);
+
+function isAlertDisabled(alert: Alert) {
return alert.enabled === false;
}
-function isAlertMuted(alert: AlertTableItem) {
+function isAlertMuted(alert: Alert) {
return alert.muteAll === true;
}
+
+function noop() {}
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx
new file mode 100644
index 0000000000000..d0fd0e1792818
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiButton, EuiFormRow, EuiPopover } from '@elastic/eui';
+
+export const BulkOperationPopover: React.FunctionComponent = ({ children }) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ return (
+ setIsPopoverOpen(false)}
+ data-test-subj="bulkAction"
+ button={
+ setIsPopoverOpen(!isPopoverOpen)}
+ >
+
+
+ }
+ >
+ {children &&
+ React.Children.map(children, child =>
+ React.isValidElement(child) ? (
+ {React.cloneElement(child, {})}
+ ) : (
+ child
+ )
+ )}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx
new file mode 100644
index 0000000000000..dd6b8775ba3d0
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import * as React from 'react';
+import { shallow, mount } from 'enzyme';
+import { withActionOperations, ComponentOpts } from './with_actions_api_operations';
+import * as actionApis from '../../../lib/action_connector_api';
+import { useAppDependencies } from '../../../app_context';
+
+jest.mock('../../../lib/action_connector_api');
+
+jest.mock('../../../app_context', () => {
+ const http = jest.fn();
+ return {
+ useAppDependencies: jest.fn(() => ({
+ http,
+ })),
+ };
+});
+
+describe('with_action_api_operations', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('extends any component with Action Api methods', () => {
+ const ComponentToExtend = (props: ComponentOpts) => {
+ expect(typeof props.loadActionTypes).toEqual('function');
+ return ;
+ };
+
+ const ExtendedComponent = withActionOperations(ComponentToExtend);
+ expect(shallow().type()).toEqual(ComponentToExtend);
+ });
+
+ it('loadActionTypes calls the loadActionTypes api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ loadActionTypes }: ComponentOpts) => {
+ return ;
+ };
+
+ const ExtendedComponent = withActionOperations(ComponentToExtend);
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(actionApis.loadActionTypes).toHaveBeenCalledTimes(1);
+ expect(actionApis.loadActionTypes).toHaveBeenCalledWith({ http });
+ });
+});
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx
new file mode 100644
index 0000000000000..45e6c6b10532c
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { ActionType } from '../../../../types';
+import { useAppDependencies } from '../../../app_context';
+import { loadActionTypes } from '../../../lib/action_connector_api';
+
+export interface ComponentOpts {
+ loadActionTypes: () => Promise;
+}
+
+export type PropsWithOptionalApiHandlers = Omit & Partial;
+
+export function withActionOperations(
+ WrappedComponent: React.ComponentType
+): React.FunctionComponent> {
+ return (props: PropsWithOptionalApiHandlers) => {
+ const { http } = useAppDependencies();
+ return (
+ loadActionTypes({ http })} />
+ );
+ };
+}
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx
new file mode 100644
index 0000000000000..30a065479ce33
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx
@@ -0,0 +1,269 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import * as React from 'react';
+import { shallow, mount } from 'enzyme';
+import uuid from 'uuid';
+import { withBulkAlertOperations, ComponentOpts } from './with_bulk_alert_api_operations';
+import * as alertApi from '../../../lib/alert_api';
+import { useAppDependencies } from '../../../app_context';
+import { Alert } from '../../../../types';
+
+jest.mock('../../../lib/alert_api');
+
+jest.mock('../../../app_context', () => {
+ const http = jest.fn();
+ return {
+ useAppDependencies: jest.fn(() => ({
+ http,
+ legacy: {
+ capabilities: {
+ get: jest.fn(() => ({})),
+ },
+ },
+ })),
+ };
+});
+
+describe('with_bulk_alert_api_operations', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('extends any component with AlertApi methods', () => {
+ const ComponentToExtend = (props: ComponentOpts) => {
+ expect(typeof props.muteAlerts).toEqual('function');
+ expect(typeof props.unmuteAlerts).toEqual('function');
+ expect(typeof props.enableAlerts).toEqual('function');
+ expect(typeof props.disableAlerts).toEqual('function');
+ expect(typeof props.deleteAlerts).toEqual('function');
+ expect(typeof props.muteAlert).toEqual('function');
+ expect(typeof props.unmuteAlert).toEqual('function');
+ expect(typeof props.enableAlert).toEqual('function');
+ expect(typeof props.disableAlert).toEqual('function');
+ expect(typeof props.deleteAlert).toEqual('function');
+ expect(typeof props.loadAlert).toEqual('function');
+ expect(typeof props.loadAlertTypes).toEqual('function');
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ expect(shallow().type()).toEqual(ComponentToExtend);
+ });
+
+ // single alert
+ it('muteAlert calls the muteAlert api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ muteAlert, alert }: ComponentOpts & { alert: Alert }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alert = mockAlert();
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.muteAlert).toHaveBeenCalledTimes(1);
+ expect(alertApi.muteAlert).toHaveBeenCalledWith({ id: alert.id, http });
+ });
+
+ it('unmuteAlert calls the unmuteAlert api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ unmuteAlert, alert }: ComponentOpts & { alert: Alert }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alert = mockAlert({ muteAll: true });
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.unmuteAlert).toHaveBeenCalledTimes(1);
+ expect(alertApi.unmuteAlert).toHaveBeenCalledWith({ id: alert.id, http });
+ });
+
+ it('enableAlert calls the muteAlerts api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ enableAlert, alert }: ComponentOpts & { alert: Alert }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alert = mockAlert({ enabled: false });
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.enableAlert).toHaveBeenCalledTimes(1);
+ expect(alertApi.enableAlert).toHaveBeenCalledWith({ id: alert.id, http });
+ });
+
+ it('disableAlert calls the disableAlert api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ disableAlert, alert }: ComponentOpts & { alert: Alert }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alert = mockAlert();
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.disableAlert).toHaveBeenCalledTimes(1);
+ expect(alertApi.disableAlert).toHaveBeenCalledWith({ id: alert.id, http });
+ });
+
+ it('deleteAlert calls the deleteAlert api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ deleteAlert, alert }: ComponentOpts & { alert: Alert }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alert = mockAlert();
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.deleteAlert).toHaveBeenCalledTimes(1);
+ expect(alertApi.deleteAlert).toHaveBeenCalledWith({ id: alert.id, http });
+ });
+
+ // bulk alerts
+ it('muteAlerts calls the muteAlerts api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ muteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alerts = [mockAlert(), mockAlert()];
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.muteAlerts).toHaveBeenCalledTimes(1);
+ expect(alertApi.muteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http });
+ });
+
+ it('unmuteAlerts calls the unmuteAlerts api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ unmuteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alerts = [mockAlert({ muteAll: true }), mockAlert({ muteAll: true })];
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.unmuteAlerts).toHaveBeenCalledTimes(1);
+ expect(alertApi.unmuteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http });
+ });
+
+ it('enableAlerts calls the muteAlertss api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ enableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alerts = [
+ mockAlert({ enabled: false }),
+ mockAlert({ enabled: true }),
+ mockAlert({ enabled: false }),
+ ];
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.enableAlerts).toHaveBeenCalledTimes(1);
+ expect(alertApi.enableAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[2].id], http });
+ });
+
+ it('disableAlerts calls the disableAlerts api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ disableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alerts = [mockAlert(), mockAlert()];
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.disableAlerts).toHaveBeenCalledTimes(1);
+ expect(alertApi.disableAlerts).toHaveBeenCalledWith({
+ ids: [alerts[0].id, alerts[1].id],
+ http,
+ });
+ });
+
+ it('deleteAlerts calls the deleteAlerts api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ deleteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alerts = [mockAlert(), mockAlert()];
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.deleteAlerts).toHaveBeenCalledTimes(1);
+ expect(alertApi.deleteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http });
+ });
+
+ it('loadAlert calls the loadAlert api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({
+ loadAlert,
+ alertId,
+ }: ComponentOpts & { alertId: Alert['id'] }) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const alertId = uuid.v4();
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.loadAlert).toHaveBeenCalledTimes(1);
+ expect(alertApi.loadAlert).toHaveBeenCalledWith({ alertId, http });
+ });
+
+ it('loadAlertTypes calls the loadAlertTypes api', () => {
+ const { http } = useAppDependencies();
+ const ComponentToExtend = ({ loadAlertTypes }: ComponentOpts) => {
+ return ;
+ };
+
+ const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
+ const component = mount();
+ component.find('button').simulate('click');
+
+ expect(alertApi.loadAlertTypes).toHaveBeenCalledTimes(1);
+ expect(alertApi.loadAlertTypes).toHaveBeenCalledWith({ http });
+ });
+});
+
+function mockAlert(overloads: Partial = {}): Alert {
+ return {
+ id: uuid.v4(),
+ enabled: true,
+ name: `alert-${uuid.v4()}`,
+ tags: [],
+ alertTypeId: '.noop',
+ consumer: 'consumer',
+ schedule: { interval: '1m' },
+ actions: [],
+ params: {},
+ createdBy: null,
+ updatedBy: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ apiKeyOwner: null,
+ throttle: null,
+ muteAll: false,
+ mutedInstanceIds: [],
+ ...overloads,
+ };
+}
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx
new file mode 100644
index 0000000000000..c61ba631ab868
--- /dev/null
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx
@@ -0,0 +1,103 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { Alert, AlertType } from '../../../../types';
+import { useAppDependencies } from '../../../app_context';
+import {
+ deleteAlerts,
+ disableAlerts,
+ enableAlerts,
+ muteAlerts,
+ unmuteAlerts,
+ deleteAlert,
+ disableAlert,
+ enableAlert,
+ muteAlert,
+ unmuteAlert,
+ loadAlert,
+ loadAlertTypes,
+} from '../../../lib/alert_api';
+
+export interface ComponentOpts {
+ muteAlerts: (alerts: Alert[]) => Promise;
+ unmuteAlerts: (alerts: Alert[]) => Promise;
+ enableAlerts: (alerts: Alert[]) => Promise;
+ disableAlerts: (alerts: Alert[]) => Promise;
+ deleteAlerts: (alerts: Alert[]) => Promise;
+ muteAlert: (alert: Alert) => Promise;
+ unmuteAlert: (alert: Alert) => Promise;
+ enableAlert: (alert: Alert) => Promise;
+ disableAlert: (alert: Alert) => Promise;
+ deleteAlert: (alert: Alert) => Promise;
+ loadAlert: (id: Alert['id']) => Promise;
+ loadAlertTypes: () => Promise;
+}
+
+export type PropsWithOptionalApiHandlers = Omit & Partial;
+
+export function withBulkAlertOperations(
+ WrappedComponent: React.ComponentType
+): React.FunctionComponent> {
+ return (props: PropsWithOptionalApiHandlers) => {
+ const { http } = useAppDependencies();
+ return (
+
+ muteAlerts({ http, ids: items.filter(item => !isAlertMuted(item)).map(item => item.id) })
+ }
+ unmuteAlerts={async (items: Alert[]) =>
+ unmuteAlerts({ http, ids: items.filter(isAlertMuted).map(item => item.id) })
+ }
+ enableAlerts={async (items: Alert[]) =>
+ enableAlerts({ http, ids: items.filter(isAlertDisabled).map(item => item.id) })
+ }
+ disableAlerts={async (items: Alert[]) =>
+ disableAlerts({
+ http,
+ ids: items.filter(item => !isAlertDisabled(item)).map(item => item.id),
+ })
+ }
+ deleteAlerts={async (items: Alert[]) =>
+ deleteAlerts({ http, ids: items.map(item => item.id) })
+ }
+ muteAlert={async (alert: Alert) => {
+ if (!isAlertMuted(alert)) {
+ return muteAlert({ http, id: alert.id });
+ }
+ }}
+ unmuteAlert={async (alert: Alert) => {
+ if (isAlertMuted(alert)) {
+ return unmuteAlert({ http, id: alert.id });
+ }
+ }}
+ enableAlert={async (alert: Alert) => {
+ if (isAlertDisabled(alert)) {
+ return enableAlert({ http, id: alert.id });
+ }
+ }}
+ disableAlert={async (alert: Alert) => {
+ if (!isAlertDisabled(alert)) {
+ return disableAlert({ http, id: alert.id });
+ }
+ }}
+ deleteAlert={async (alert: Alert) => deleteAlert({ http, id: alert.id })}
+ loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })}
+ loadAlertTypes={async () => loadAlertTypes({ http })}
+ />
+ );
+ };
+}
+
+function isAlertDisabled(alert: Alert) {
+ return alert.enabled === false;
+}
+
+function isAlertMuted(alert: Alert) {
+ return alert.muteAll === true;
+}
diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts
index ed63ade903104..7fb7d0bf48e4d 100644
--- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts
+++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts
@@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TypeRegistry } from './application/type_registry';
-import { SanitizedAlert as Alert } from '../../../alerting/common';
-export { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common';
+import { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common';
+import { ActionType } from '../../../../../plugins/actions/common';
+
+export { Alert, AlertAction };
+export { ActionType };
export type ActionTypeIndex = Record;
export type AlertTypeIndex = Record;
@@ -47,11 +50,6 @@ export interface ValidationResult {
errors: Record;
}
-export interface ActionType {
- id: string;
- name: string;
-}
-
export interface ActionConnector {
secrets: Record;
id: string;
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
new file mode 100644
index 0000000000000..e8ed54571c77c
--- /dev/null
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
@@ -0,0 +1,146 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+import uuid from 'uuid';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default ({ getPageObjects, getService }: FtrProviderContext) => {
+ const testSubjects = getService('testSubjects');
+ const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'alertDetailsUI']);
+ const browser = getService('browser');
+ const alerting = getService('alerting');
+
+ describe('Alert Details', function() {
+ const testRunUuid = uuid.v4();
+
+ before(async () => {
+ await pageObjects.common.navigateToApp('triggersActions');
+
+ const actions = await Promise.all([
+ alerting.actions.createAction({
+ name: `server-log-${testRunUuid}-${0}`,
+ actionTypeId: '.server-log',
+ config: {},
+ secrets: {},
+ }),
+ alerting.actions.createAction({
+ name: `server-log-${testRunUuid}-${1}`,
+ actionTypeId: '.server-log',
+ config: {},
+ secrets: {},
+ }),
+ ]);
+
+ const alert = await alerting.alerts.createAlwaysFiringWithActions(
+ `test-alert-${testRunUuid}`,
+ actions.map(action => ({
+ id: action.id,
+ group: 'default',
+ params: {
+ message: 'from alert 1s',
+ level: 'warn',
+ },
+ }))
+ );
+
+ // refresh to see alert
+ await browser.refresh();
+
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ // Verify content
+ await testSubjects.existOrFail('alertsList');
+
+ // click on first alert
+ await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name);
+ });
+
+ it('renders the alert details', async () => {
+ const headingText = await pageObjects.alertDetailsUI.getHeadingText();
+ expect(headingText).to.be(`test-alert-${testRunUuid}`);
+
+ const alertType = await pageObjects.alertDetailsUI.getAlertType();
+ expect(alertType).to.be(`Always Firing`);
+
+ const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels();
+ expect(actionType).to.be(`Server log`);
+ expect(actionCount).to.be(`+1`);
+ });
+
+ it('should disable the alert', async () => {
+ const enableSwitch = await testSubjects.find('enableSwitch');
+
+ const isChecked = await enableSwitch.getAttribute('aria-checked');
+ expect(isChecked).to.eql('true');
+
+ await enableSwitch.click();
+
+ const enabledSwitchAfterDisabling = await testSubjects.find('enableSwitch');
+ const isCheckedAfterDisabling = await enabledSwitchAfterDisabling.getAttribute(
+ 'aria-checked'
+ );
+ expect(isCheckedAfterDisabling).to.eql('false');
+ });
+
+ it('shouldnt allow you to mute a disabled alert', async () => {
+ const disabledEnableSwitch = await testSubjects.find('enableSwitch');
+ expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false');
+
+ const muteSwitch = await testSubjects.find('muteSwitch');
+ expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false');
+
+ await muteSwitch.click();
+
+ const muteSwitchAfterTryingToMute = await testSubjects.find('muteSwitch');
+ const isDisabledMuteAfterDisabling = await muteSwitchAfterTryingToMute.getAttribute(
+ 'aria-checked'
+ );
+ expect(isDisabledMuteAfterDisabling).to.eql('false');
+ });
+
+ it('should reenable a disabled the alert', async () => {
+ const enableSwitch = await testSubjects.find('enableSwitch');
+
+ const isChecked = await enableSwitch.getAttribute('aria-checked');
+ expect(isChecked).to.eql('false');
+
+ await enableSwitch.click();
+
+ const enabledSwitchAfterReenabling = await testSubjects.find('enableSwitch');
+ const isCheckedAfterDisabling = await enabledSwitchAfterReenabling.getAttribute(
+ 'aria-checked'
+ );
+ expect(isCheckedAfterDisabling).to.eql('true');
+ });
+
+ it('should mute the alert', async () => {
+ const muteSwitch = await testSubjects.find('muteSwitch');
+
+ const isChecked = await muteSwitch.getAttribute('aria-checked');
+ expect(isChecked).to.eql('false');
+
+ await muteSwitch.click();
+
+ const muteSwitchAfterDisabling = await testSubjects.find('muteSwitch');
+ const isCheckedAfterDisabling = await muteSwitchAfterDisabling.getAttribute('aria-checked');
+ expect(isCheckedAfterDisabling).to.eql('true');
+ });
+
+ it('should unmute the alert', async () => {
+ const muteSwitch = await testSubjects.find('muteSwitch');
+
+ const isChecked = await muteSwitch.getAttribute('aria-checked');
+ expect(isChecked).to.eql('true');
+
+ await muteSwitch.click();
+
+ const muteSwitchAfterUnmuting = await testSubjects.find('muteSwitch');
+ const isCheckedAfterDisabling = await muteSwitchAfterUnmuting.getAttribute('aria-checked');
+ expect(isCheckedAfterDisabling).to.eql('false');
+ });
+ });
+};
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts
index 13f50a505b0b6..307f39382a236 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts
@@ -12,6 +12,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
const log = getService('log');
const browser = getService('browser');
+ const alerting = getService('alerting');
describe('Home page', function() {
before(async () => {
@@ -55,6 +56,43 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
// Verify content
await testSubjects.existOrFail('alertsList');
});
+
+ it('navigates to an alert details page', async () => {
+ const action = await alerting.actions.createAction({
+ name: `server-log-${Date.now()}`,
+ actionTypeId: '.server-log',
+ config: {},
+ secrets: {},
+ });
+
+ const alert = await alerting.alerts.createAlwaysFiringWithAction(
+ `test-alert-${Date.now()}`,
+ {
+ id: action.id,
+ group: 'default',
+ params: {
+ message: 'from alert 1s',
+ level: 'warn',
+ },
+ }
+ );
+
+ // refresh to see alert
+ await browser.refresh();
+
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ // Verify content
+ await testSubjects.existOrFail('alertsList');
+
+ // click on first alert
+ await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name);
+
+ // Verify url
+ expect(await browser.getCurrentUrl()).to.contain(`/alert/${alert.id}`);
+
+ await alerting.alerts.deleteAlert(alert.id);
+ });
});
});
};
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts
index c76f477c8cfbe..a771fbf85e0b6 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts
@@ -12,5 +12,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
loadTestFile(require.resolve('./home_page'));
loadTestFile(require.resolve('./connectors'));
loadTestFile(require.resolve('./alerts'));
+ loadTestFile(require.resolve('./details'));
});
};
diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts
index df651c67c2c28..43162e9256370 100644
--- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts
+++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts
@@ -12,13 +12,42 @@ export default function(kibana: any) {
require: ['alerting'],
name: 'alerts',
init(server: any) {
- const noopAlertType: AlertType = {
- id: 'test.noop',
- name: 'Test: Noop',
- actionGroups: ['default'],
- async executor() {},
- };
- server.plugins.alerting.setup.registerType(noopAlertType);
+ createNoopAlertType(server.plugins.alerting.setup);
+ createAlwaysFiringAlertType(server.plugins.alerting.setup);
},
});
}
+
+function createNoopAlertType(setupContract: any) {
+ const noopAlertType: AlertType = {
+ id: 'test.noop',
+ name: 'Test: Noop',
+ actionGroups: ['default'],
+ async executor() {},
+ };
+ setupContract.registerType(noopAlertType);
+}
+
+function createAlwaysFiringAlertType(setupContract: any) {
+ // Alert types
+ const alwaysFiringAlertType: any = {
+ id: 'test.always-firing',
+ name: 'Always Firing',
+ actionGroups: ['default', 'other'],
+ async executor(alertExecutorOptions: any) {
+ const { services, state } = alertExecutorOptions;
+
+ services
+ .alertInstanceFactory('1')
+ .replaceState({ instanceStateValue: true })
+ .scheduleActions('default', {
+ instanceContextValue: true,
+ });
+ return {
+ globalStateValue: true,
+ groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1,
+ };
+ },
+ };
+ setupContract.registerType(alwaysFiringAlertType);
+}
diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts
new file mode 100644
index 0000000000000..6d2038a6ba04c
--- /dev/null
+++ b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { FtrProviderContext } from '../ftr_provider_context';
+
+export function AlertDetailsPageProvider({ getService }: FtrProviderContext) {
+ const testSubjects = getService('testSubjects');
+
+ return {
+ async getHeadingText() {
+ return await testSubjects.getVisibleText('alertDetailsTitle');
+ },
+ async getAlertType() {
+ return await testSubjects.getVisibleText('alertTypeLabel');
+ },
+ async getActionsLabels() {
+ return {
+ actionType: await testSubjects.getVisibleText('actionTypeLabel'),
+ actionCount: await testSubjects.getVisibleText('actionCountLabel'),
+ };
+ },
+ };
+}
diff --git a/x-pack/test/functional_with_es_ssl/page_objects/index.ts b/x-pack/test/functional_with_es_ssl/page_objects/index.ts
index a068ba7dfe81d..cfc44221a9c17 100644
--- a/x-pack/test/functional_with_es_ssl/page_objects/index.ts
+++ b/x-pack/test/functional_with_es_ssl/page_objects/index.ts
@@ -6,8 +6,10 @@
import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects';
import { TriggersActionsPageProvider } from './triggers_actions_ui_page';
+import { AlertDetailsPageProvider } from './alert_details';
export const pageObjects = {
...xpackFunctionalPageObjects,
triggersActionsUI: TriggersActionsPageProvider,
+ alertDetailsUI: AlertDetailsPageProvider,
};
diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts
index a04ecc969a7e1..ae66ac0ddddfb 100644
--- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts
+++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts
@@ -92,6 +92,10 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext)
};
});
},
+ async clickOnAlertInAlertsList(name: string) {
+ await this.searchAlerts(name);
+ await find.clickDisplayedByCssSelector(`[data-test-subj="alertsList"] [title="${name}"]`);
+ },
async changeTabs(tab: 'alertsTab' | 'connectorsTab') {
return await testSubjects.click(tab);
},
diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts
new file mode 100644
index 0000000000000..9454a32757068
--- /dev/null
+++ b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import axios, { AxiosInstance } from 'axios';
+import util from 'util';
+import { ToolingLog } from '@kbn/dev-utils';
+
+export class Actions {
+ private log: ToolingLog;
+ private axios: AxiosInstance;
+
+ constructor(url: string, log: ToolingLog) {
+ this.log = log;
+ this.axios = axios.create({
+ headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/actions' },
+ baseURL: url,
+ maxRedirects: 0,
+ validateStatus: () => true, // we do our own validation below and throw better error messages
+ });
+ }
+
+ public async createAction(actionParams: {
+ name: string;
+ actionTypeId: string;
+ config: Record;
+ secrets: Record;
+ }) {
+ this.log.debug(`creating action ${actionParams.name}`);
+
+ const { data: action, status: actionStatus, actionStatusText } = await this.axios.post(
+ `/api/action`,
+ actionParams
+ );
+ if (actionStatus !== 200) {
+ throw new Error(
+ `Expected status code of 200, received ${actionStatus} ${actionStatusText}: ${util.inspect(
+ action
+ )}`
+ );
+ }
+
+ this.log.debug(`created action ${action.id}`);
+ return action;
+ }
+}
diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts
new file mode 100644
index 0000000000000..1a31d4796d5bc
--- /dev/null
+++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import axios, { AxiosInstance } from 'axios';
+import util from 'util';
+import { ToolingLog } from '@kbn/dev-utils';
+
+export class Alerts {
+ private log: ToolingLog;
+ private axios: AxiosInstance;
+
+ constructor(url: string, log: ToolingLog) {
+ this.log = log;
+ this.axios = axios.create({
+ headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/alerts' },
+ baseURL: url,
+ maxRedirects: 0,
+ validateStatus: () => true, // we do our own validation below and throw better error messages
+ });
+ }
+
+ public async createAlwaysFiringWithActions(
+ name: string,
+ actions: Array<{
+ id: string;
+ group: string;
+ params: Record;
+ }>
+ ) {
+ this.log.debug(`creating alert ${name}`);
+
+ const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, {
+ enabled: true,
+ name,
+ tags: ['foo'],
+ alertTypeId: 'test.always-firing',
+ consumer: 'bar',
+ schedule: { interval: '1m' },
+ throttle: '1m',
+ actions,
+ params: {},
+ });
+ if (status !== 200) {
+ throw new Error(
+ `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}`
+ );
+ }
+
+ this.log.debug(`created alert ${alert.id}`);
+
+ return alert;
+ }
+
+ public async createAlwaysFiringWithAction(
+ name: string,
+ action: {
+ id: string;
+ group: string;
+ params: Record;
+ }
+ ) {
+ return this.createAlwaysFiringWithActions(name, [action]);
+ }
+
+ public async deleteAlert(id: string) {
+ this.log.debug(`deleting alert ${id}`);
+
+ const { data: alert, status, statusText } = await this.axios.delete(`/api/alert/${id}`);
+ if (status !== 204) {
+ throw new Error(
+ `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(alert)}`
+ );
+ }
+ this.log.debug(`deleted alert ${alert.id}`);
+ }
+}
diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/index.ts b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts
new file mode 100644
index 0000000000000..e0aa827316c01
--- /dev/null
+++ b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { format as formatUrl } from 'url';
+
+import { Alerts } from './alerts';
+import { Actions } from './actions';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export function AlertsServiceProvider({ getService }: FtrProviderContext) {
+ const log = getService('log');
+ const config = getService('config');
+ const url = formatUrl(config.get('servers.kibana'));
+
+ return new (class AlertingService {
+ actions = new Actions(url, log);
+ alerts = new Alerts(url, log);
+ })();
+}
diff --git a/x-pack/test/functional_with_es_ssl/services/index.ts b/x-pack/test/functional_with_es_ssl/services/index.ts
index 6e96921c25a31..f04c2c980055d 100644
--- a/x-pack/test/functional_with_es_ssl/services/index.ts
+++ b/x-pack/test/functional_with_es_ssl/services/index.ts
@@ -5,7 +5,9 @@
*/
import { services as xpackFunctionalServices } from '../../functional/services';
+import { AlertsServiceProvider } from './alerting';
export const services = {
...xpackFunctionalServices,
+ alerting: AlertsServiceProvider,
};