From b7f1b47966481147b66565cad4ca0ec750f78172 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 2 Mar 2020 17:16:54 +1300 Subject: [PATCH 01/45] added registration of navigation by consumer and alert type --- .../alert_navigation_registry.mock.ts | 22 ++ .../alert_navigation_registry.test.ts | 110 +++++++++ .../alert_navigation_registry.ts | 75 ++++++ x-pack/plugins/alerting/server/plugin.ts | 17 ++ .../plugins/alerting/server/routes/index.ts | 1 + .../alerting/server/routes/navigation.test.ts | 226 ++++++++++++++++++ .../alerting/server/routes/navigation.ts | 58 +++++ .../common/fixtures/plugins/alerts/index.ts | 5 + .../tests/alerting/index.ts | 1 + .../tests/alerting/navigation.ts | 162 +++++++++++++ .../spaces_only/tests/alerting/index.ts | 1 + .../spaces_only/tests/alerting/navigation.ts | 78 ++++++ 12 files changed, 756 insertions(+) create mode 100644 x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.mock.ts create mode 100644 x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.test.ts create mode 100644 x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts create mode 100644 x-pack/plugins/alerting/server/routes/navigation.test.ts create mode 100644 x-pack/plugins/alerting/server/routes/navigation.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/navigation.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/navigation.ts diff --git a/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.mock.ts b/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.mock.ts new file mode 100644 index 0000000000000..bf78ca9f89554 --- /dev/null +++ b/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.mock.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 { AlertNavigationRegistry } from './alert_navigation_registry'; + +type Schema = PublicMethodsOf; + +const createAlertNavigationRegistryMock = () => { + const mocked: jest.Mocked = { + has: jest.fn(), + register: jest.fn(), + get: jest.fn(), + }; + return mocked; +}; + +export const alertNavigationRegistryMock = { + create: createAlertNavigationRegistryMock, +}; diff --git a/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.test.ts b/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.test.ts new file mode 100644 index 0000000000000..23b8353bdf26c --- /dev/null +++ b/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.test.ts @@ -0,0 +1,110 @@ +/* + * 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 { AlertNavigationRegistry } from './alert_navigation_registry'; +import { AlertType, SanitizedAlert } from '../types'; +import uuid from 'uuid'; + +beforeEach(() => jest.resetAllMocks()); + +const mockAlertType = (id: string): AlertType => ({ + id, + name: id, + actionGroups: [], + defaultActionGroupId: 'default', + executor: jest.fn(), +}); + +describe('AlertNavigationRegistry', () => { + function handler(alert: SanitizedAlert, alertType: AlertType) { + return {}; + } + + describe('has()', () => { + test('returns false for unregistered consumer handlers', () => { + const registry = new AlertNavigationRegistry(); + expect(registry.has('siem', mockAlertType(uuid.v4()))).toEqual(false); + }); + + test('returns false for unregistered alert types handlers', () => { + const registry = new AlertNavigationRegistry(); + expect(registry.has('siem', mockAlertType('index_threshold'))).toEqual(false); + }); + + test('returns true for registered consumer & alert types handlers', () => { + const registry = new AlertNavigationRegistry(); + const alertType = mockAlertType('index_threshold'); + registry.register('siem', alertType, handler); + expect(registry.has('siem', alertType)).toEqual(true); + }); + }); + + describe('register()', () => { + test('registers a handler by consumer & Alert Type', () => { + const registry = new AlertNavigationRegistry(); + const alertType = mockAlertType('index_threshold'); + registry.register('siem', alertType, handler); + expect(registry.has('siem', alertType)).toEqual(true); + }); + + test('allows registeration of multiple handlers for the same consumer', () => { + const registry = new AlertNavigationRegistry(); + + const indexThresholdAlertType = mockAlertType('index_threshold'); + registry.register('siem', indexThresholdAlertType, handler); + expect(registry.has('siem', indexThresholdAlertType)).toEqual(true); + + const geoAlertType = mockAlertType('geogrid'); + registry.register('siem', geoAlertType, handler); + expect(registry.has('siem', geoAlertType)).toEqual(true); + }); + + test('allows registeration of multiple handlers for the same Alert Type', () => { + const registry = new AlertNavigationRegistry(); + + const indexThresholdAlertType = mockAlertType('geogrid'); + registry.register('siem', indexThresholdAlertType, handler); + expect(registry.has('siem', indexThresholdAlertType)).toEqual(true); + + registry.register('apm', indexThresholdAlertType, handler); + expect(registry.has('apm', indexThresholdAlertType)).toEqual(true); + }); + + test('throws if an existing handler is registered', () => { + const registry = new AlertNavigationRegistry(); + const alertType = mockAlertType('index_threshold'); + registry.register('siem', alertType, handler); + expect(() => { + registry.register('siem', alertType, handler); + }).toThrowErrorMatchingInlineSnapshot( + `"Navigation for Alert type \\"index_threshold\\" within \\"siem\\" is already registered."` + ); + }); + }); + + describe('get()', () => { + test('returns registered handlers by consumer & Alert Type', () => { + const registry = new AlertNavigationRegistry(); + + function indexThresholdHandler(alert: SanitizedAlert, alertType: AlertType) { + return {}; + } + + const indexThresholdAlertType = mockAlertType('geogrid'); + registry.register('siem', indexThresholdAlertType, indexThresholdHandler); + expect(registry.get('siem', indexThresholdAlertType)).toEqual(indexThresholdHandler); + }); + + test('throws if a handler isnt registered', () => { + const registry = new AlertNavigationRegistry(); + const alertType = mockAlertType('index_threshold'); + + expect(() => registry.get('siem', alertType)).toThrowErrorMatchingInlineSnapshot( + `"Navigation for Alert type \\"index_threshold\\" within \\"siem\\" is not registered."` + ); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts b/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts new file mode 100644 index 0000000000000..52eae87def3cf --- /dev/null +++ b/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts @@ -0,0 +1,75 @@ +/* + * 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 Boom from 'boom'; +import { i18n } from '@kbn/i18n'; +import { AlertType, SanitizedAlert } from '../types'; +import { AlertInstances } from '../alert_instance/alert_instance'; + +interface AlertNavigationContext { + filter?: string; + dateRange?: { + start: Date; + end: Date; + }; +} + +export type AlertNavigationHandler = ( + alert: SanitizedAlert, + alertType: AlertType, + alertInstances?: AlertInstances[], + context?: AlertNavigationContext +) => Record | string; + +export class AlertNavigationRegistry { + private readonly alertNavigations: Map> = new Map(); + + public has(consumer: string, alertType: AlertType) { + return this.alertNavigations.get(consumer)?.has(alertType.id) ?? false; + } + + private createConsumerNavigation(consumer: string) { + const consumerNavigations = new Map(); + this.alertNavigations.set(consumer, consumerNavigations); + return consumerNavigations; + } + + public register(consumer: string, alertType: AlertType, handler: AlertNavigationHandler) { + if (this.has(consumer, alertType)) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.alertNavigationRegistry.get.missingNavigationError', { + defaultMessage: + 'Navigation for Alert type "{alertType}" within "{consumer}" is already registered.', + values: { + alertType: alertType.id, + consumer, + }, + }) + ); + } + + const consumerNavigations = + this.alertNavigations.get(consumer) ?? this.createConsumerNavigation(consumer); + + consumerNavigations.set(alertType.id, handler); + } + + public get(consumer: string, alertType: AlertType): AlertNavigationHandler { + if (!this.has(consumer, alertType)) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.alertNavigationRegistry.get.missingNavigationError', { + defaultMessage: + 'Navigation for Alert type "{alertType}" within "{consumer}" is not registered.', + values: { + alertType: alertType.id, + consumer, + }, + }) + ); + } + return this.alertNavigations.get(consumer)!.get(alertType.id)!; + } +} diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index bed163878b5ac..5594a6fcab6ea 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -43,6 +43,7 @@ import { unmuteAllAlertRoute, muteAlertInstanceRoute, unmuteAlertInstanceRoute, + getAlertNavigationRoute, } from './routes'; import { LicensingPluginSetup } from '../../licensing/server'; import { @@ -50,9 +51,18 @@ import { PluginStartContract as ActionsPluginStartContract, } from '../../../plugins/actions/server'; import { Services } from './types'; +import { + AlertNavigationRegistry, + AlertNavigationHandler, +} from './alert_navigation_registry/alert_navigation_registry'; export interface PluginSetupContract { registerType: AlertTypeRegistry['register']; + registerNavigation: ( + consumer: string, + alertType: string, + handler: AlertNavigationHandler + ) => void; } export interface PluginStartContract { listTypes: AlertTypeRegistry['list']; @@ -76,6 +86,7 @@ export interface AlertingPluginsStart { export class AlertingPlugin { private readonly logger: Logger; private alertTypeRegistry?: AlertTypeRegistry; + private alertNavigationRegistry?: AlertNavigationRegistry; private readonly taskRunnerFactory: TaskRunnerFactory; private adminClient?: IClusterClient; private serverBasePath?: string; @@ -122,6 +133,9 @@ export class AlertingPlugin { taskRunnerFactory: this.taskRunnerFactory, }); this.alertTypeRegistry = alertTypeRegistry; + const alertNavigationRegistry = new AlertNavigationRegistry(); + this.alertNavigationRegistry = alertNavigationRegistry; + this.serverBasePath = core.http.basePath.serverBasePath; core.http.registerRouteHandlerContext('alerting', this.createRouteHandlerContext()); @@ -134,6 +148,7 @@ export class AlertingPlugin { findAlertRoute(router, this.licenseState); getAlertRoute(router, this.licenseState); getAlertStateRoute(router, this.licenseState); + getAlertNavigationRoute(router, this.licenseState, alertTypeRegistry, alertNavigationRegistry); listAlertTypesRoute(router, this.licenseState); updateAlertRoute(router, this.licenseState); enableAlertRoute(router, this.licenseState); @@ -146,6 +161,8 @@ export class AlertingPlugin { return { registerType: alertTypeRegistry.register.bind(alertTypeRegistry), + registerNavigation: (consumer: string, alertType: string, handler: AlertNavigationHandler) => + alertNavigationRegistry.register(consumer, alertTypeRegistry.get(alertType), handler), }; } diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index 7ec901ae685c4..4d2aad8372f99 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -8,6 +8,7 @@ export { createAlertRoute } from './create'; export { deleteAlertRoute } from './delete'; export { findAlertRoute } from './find'; export { getAlertRoute } from './get'; +export { getAlertNavigationRoute } from './navigation'; export { getAlertStateRoute } from './get_alert_state'; export { listAlertTypesRoute } from './list_alert_types'; export { updateAlertRoute } from './update'; diff --git a/x-pack/plugins/alerting/server/routes/navigation.test.ts b/x-pack/plugins/alerting/server/routes/navigation.test.ts new file mode 100644 index 0000000000000..225d252420f53 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/navigation.test.ts @@ -0,0 +1,226 @@ +/* + * 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 { getAlertNavigationRoute } from './navigation'; +import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { mockLicenseState } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; +import { alertNavigationRegistryMock } from '../alert_navigation_registry/alert_navigation_registry.mock'; +import { alertTypeRegistryMock } from '../alert_type_registry.mock'; +import { SanitizedAlert, AlertType } from '../types'; + +const alertsClient = alertsClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +const alertNavigationRegistry = alertNavigationRegistryMock.create(); +const alertTypeRegistry = alertTypeRegistryMock.create(); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getAlertNavigationRoute', () => { + const mockedAlert = { + id: '1', + alertTypeId: 'testAlertType', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date(), + updatedAt: new Date(), + actions: [ + { + group: 'default', + id: '2', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + consumer: 'bar', + name: 'abc', + tags: ['foo'], + enabled: true, + muteAll: false, + createdBy: '', + updatedBy: '', + apiKey: '', + apiKeyOwner: '', + throttle: '30s', + mutedInstanceIds: [], + }; + + const mockedAlertType = { + id: 'testAlertType', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + executor: jest.fn(), + }; + + it('gets navigation state for an alert', async () => { + const licenseState = mockLicenseState(); + const router: RouterMock = mockRouter.create(); + + getAlertNavigationRoute(router, licenseState, alertTypeRegistry, alertNavigationRegistry); + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/consumer/{consumer}/navigation"`); + expect(config.options).toMatchInlineSnapshot(` + Object { + "tags": Array [ + "access:alerting-read", + ], + } + `); + + alertTypeRegistry.get.mockImplementation(() => mockedAlertType); + alertsClient.get.mockResolvedValue(mockedAlert); + alertNavigationRegistry.get.mockImplementationOnce( + () => (alert: SanitizedAlert, alertType: AlertType) => { + expect(alert).toMatchObject(mockedAlert); + expect(alertType).toMatchObject(mockedAlertType); + return { + alert: alert.id, + }; + } + ); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1', consumer: 'siem' }, + }, + ['ok'] + ); + await handler(context, req, res); + + expect(alertsClient.get).toHaveBeenCalledTimes(1); + expect(alertsClient.get.mock.calls[0][0].id).toEqual('1'); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('testAlertType'); + expect(alertNavigationRegistry.get).toHaveBeenCalledWith('siem', mockedAlertType); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + state: { + alert: mockedAlert.id, + }, + }, + }); + }); + + it('gets a navigation urls for an alert', async () => { + const licenseState = mockLicenseState(); + const router: RouterMock = mockRouter.create(); + + getAlertNavigationRoute(router, licenseState, alertTypeRegistry, alertNavigationRegistry); + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/consumer/{consumer}/navigation"`); + expect(config.options).toMatchInlineSnapshot(` + Object { + "tags": Array [ + "access:alerting-read", + ], + } + `); + + alertTypeRegistry.get.mockImplementation(() => mockedAlertType); + alertsClient.get.mockResolvedValue(mockedAlert); + alertNavigationRegistry.get.mockImplementationOnce( + () => (alert: SanitizedAlert, alertType: AlertType) => { + expect(alert).toMatchObject(mockedAlert); + expect(alertType).toMatchObject(mockedAlertType); + return 'https://www.elastic.co/'; + } + ); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1', consumer: 'siem' }, + }, + ['ok'] + ); + await handler(context, req, res); + + expect(alertsClient.get).toHaveBeenCalledTimes(1); + expect(alertsClient.get.mock.calls[0][0].id).toEqual('1'); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('testAlertType'); + expect(alertNavigationRegistry.get).toHaveBeenCalledWith('siem', mockedAlertType); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + url: 'https://www.elastic.co/', + }, + }); + }); + + it('ensures the license allows getting alerts', async () => { + const licenseState = mockLicenseState(); + const router: RouterMock = mockRouter.create(); + + getAlertNavigationRoute(router, licenseState, alertTypeRegistry, alertNavigationRegistry); + + const [, handler] = router.get.mock.calls[0]; + + alertTypeRegistry.get.mockImplementation(() => mockedAlertType); + alertsClient.get.mockResolvedValue(mockedAlert); + alertNavigationRegistry.get.mockImplementationOnce(() => () => { + return 'https://www.elastic.co/'; + }); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents getting alerts', async () => { + const licenseState = mockLicenseState(); + const router: RouterMock = mockRouter.create(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + getAlertNavigationRoute(router, licenseState, alertTypeRegistry, alertNavigationRegistry); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/navigation.ts b/x-pack/plugins/alerting/server/routes/navigation.ts new file mode 100644 index 0000000000000..08906e1261163 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/navigation.ts @@ -0,0 +1,58 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { + IRouter, + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from 'kibana/server'; +import { LicenseState } from '../lib/license_state'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { AlertNavigationRegistry } from '../alert_navigation_registry/alert_navigation_registry'; +import { AlertTypeRegistry } from '../alert_type_registry'; + +const paramSchema = schema.object({ + id: schema.string(), + consumer: schema.string(), +}); + +export const getAlertNavigationRoute = ( + router: IRouter, + licenseState: LicenseState, + alertTypeRegistry: PublicMethodsOf, + alertNavigationRegistry: PublicMethodsOf +) => { + router.get( + { + path: `/api/alert/{id}/consumer/{consumer}/navigation`, + validate: { + params: paramSchema, + }, + options: { + tags: ['access:alerting-read'], + }, + }, + router.handleLegacyErrors(async function( + context: RequestHandlerContext, + req: KibanaRequest, any, any, any>, + res: KibanaResponseFactory + ): Promise> { + verifyApiAccess(licenseState); + const { id, consumer } = req.params; + const alertsClient = context.alerting.getAlertsClient(); + const alert = await alertsClient.get({ id }); + const alertType = alertTypeRegistry.get(alert.alertTypeId); + const navigationHandler = alertNavigationRegistry.get(consumer, alertType); + const state = navigationHandler(alert, alertType); + return res.ok({ + body: typeof state === 'string' ? { url: state } : { state }, + }); + }) + ); +}; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index 2e7674f2b3eb7..47e2fc68f0950 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -426,6 +426,11 @@ export default function(kibana: any) { server.newPlatform.setup.plugins.alerting.registerType(validationAlertType); server.newPlatform.setup.plugins.alerting.registerType(authorizationAlertType); server.newPlatform.setup.plugins.alerting.registerType(noopAlertType); + server.newPlatform.setup.plugins.alerting.registerNavigation( + 'consumer.noop', + noopAlertType.id, + () => 'about:blank' + ); }, }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 91b0ca0a37c92..751344e6adf48 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -24,5 +24,6 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./update_api_key')); loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./navigation')); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/navigation.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/navigation.ts new file mode 100644 index 0000000000000..f68d8c5577bd7 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/navigation.ts @@ -0,0 +1,162 @@ +/* + * 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 { UserAtSpaceScenarios } from '../../scenarios'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + ESTestIndexTool, + getUrlPrefix, + getTestAlertData, + ObjectRemover, +} from '../../../common/lib'; + +// eslint-disable-next-line import/no-default-export +export default function alertNavigationTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + const retry = getService('retry'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esTestIndexTool = new ESTestIndexTool(es, retry); + + describe('alert navigation', () => { + const authorizationIndex = '.kibana-test-authorization'; + const objectRemover = new ObjectRemover(supertest); + + before(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + await es.indices.create({ index: authorizationIndex }); + }); + afterEach(() => objectRemover.removeAll()); + after(async () => { + await esTestIndexTool.destroy(); + await es.indices.delete({ index: authorizationIndex }); + }); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + + describe(scenario.id, () => { + const consumer = 'consumer.noop'; + + it('should return a navigation URL for an AlertType', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.noop', + consumer, + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + const response = await supertestWithoutAuth + .get( + `${getUrlPrefix(space.id)}/api/alert/${ + createdAlert.id + }/consumer/${consumer}/navigation` + ) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + url: 'about:blank', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`shouldn't get alert from another space`, async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + const response = await supertestWithoutAuth + .get( + `${getUrlPrefix('other')}/api/alert/${ + createdAlert.id + }/consumer/${consumer}/navigation` + ) + .auth(user.username, user.password); + + expect(response.statusCode).to.eql(404); + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'space_1_all at space1': + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: `Saved object [alert/${createdAlert.id}] not found`, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`should handle get alert request appropriately when alert doesn't exist`, async () => { + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alert/1/consumer/${consumer}/navigation`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index a0c4da361bd38..3d75a254b3ad1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -26,5 +26,6 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./alerts_space1')); loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); + loadTestFile(require.resolve('./navigation')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/navigation.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/navigation.ts new file mode 100644 index 0000000000000..ee30d83604f31 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/navigation.ts @@ -0,0 +1,78 @@ +/* + * 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 { Spaces } from '../../scenarios'; +import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('navigation', () => { + const objectRemover = new ObjectRemover(supertest); + const consumer = 'consumer.noop'; + + afterEach(() => objectRemover.removeAll()); + + it('should handle get alert navigation request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.noop', + consumer, + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alert/${ + createdAlert.id + }/consumer/${consumer}/navigation` + ); + + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + url: 'about:blank', + }); + }); + + it(`shouldn't get navigation for an alert from another space`, async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + await supertest + .get( + `${getUrlPrefix(Spaces.other.id)}/api/alert/${ + createdAlert.id + }/consumer/${consumer}/navigation` + ) + .expect(404, { + statusCode: 404, + error: 'Not Found', + message: `Saved object [alert/${createdAlert.id}] not found`, + }); + }); + + it(`should handle get alert navigation request appropriately when alert doesn't exist`, async () => { + await supertest + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/1/consumer/${consumer}/navigation`) + .expect(404, { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); + }); + }); +} From fd0aa637411a178838204a8bc24452f260c6a85c Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 2 Mar 2020 18:35:19 +1300 Subject: [PATCH 02/45] removed unused prop --- x-pack/plugins/alerting/server/mocks.ts | 1 + x-pack/plugins/alerting/server/plugin.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index 55ad722dcf881..3ec774823a2ab 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -12,6 +12,7 @@ export { alertsClientMock }; const createSetupMock = () => { const mock: jest.Mocked = { registerType: jest.fn(), + registerNavigation: jest.fn(), }; return mock; }; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 5594a6fcab6ea..1b2a8cd805aad 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -86,7 +86,6 @@ export interface AlertingPluginsStart { export class AlertingPlugin { private readonly logger: Logger; private alertTypeRegistry?: AlertTypeRegistry; - private alertNavigationRegistry?: AlertNavigationRegistry; private readonly taskRunnerFactory: TaskRunnerFactory; private adminClient?: IClusterClient; private serverBasePath?: string; @@ -134,7 +133,6 @@ export class AlertingPlugin { }); this.alertTypeRegistry = alertTypeRegistry; const alertNavigationRegistry = new AlertNavigationRegistry(); - this.alertNavigationRegistry = alertNavigationRegistry; this.serverBasePath = core.http.basePath.serverBasePath; From 73111e5643f1aa28976558f57ec7f523a46bf118 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 3 Mar 2020 08:26:40 +1300 Subject: [PATCH 03/45] use unique identifiers for i18n --- .../alert_navigation_registry/alert_navigation_registry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts b/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts index 52eae87def3cf..ba069d8834d3c 100644 --- a/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts +++ b/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts @@ -40,7 +40,7 @@ export class AlertNavigationRegistry { public register(consumer: string, alertType: AlertType, handler: AlertNavigationHandler) { if (this.has(consumer, alertType)) { throw Boom.badRequest( - i18n.translate('xpack.alerting.alertNavigationRegistry.get.missingNavigationError', { + i18n.translate('xpack.alerting.alertNavigationRegistry.register.duplicateNavigationError', { defaultMessage: 'Navigation for Alert type "{alertType}" within "{consumer}" is already registered.', values: { From 62036f37d70cd9c9934c1b57f759bbf916fb8404 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 3 Mar 2020 11:53:50 +1300 Subject: [PATCH 04/45] removed consumer from api route --- .../alerting/server/routes/navigation.test.ts | 14 ++++++------ .../alerting/server/routes/navigation.ts | 7 +++--- .../tests/alerting/navigation.ts | 14 +++--------- .../spaces_only/tests/alerting/navigation.ts | 22 ++++++------------- 4 files changed, 20 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/alerting/server/routes/navigation.test.ts b/x-pack/plugins/alerting/server/routes/navigation.test.ts index 225d252420f53..6b45e2f12f932 100644 --- a/x-pack/plugins/alerting/server/routes/navigation.test.ts +++ b/x-pack/plugins/alerting/server/routes/navigation.test.ts @@ -46,7 +46,7 @@ describe('getAlertNavigationRoute', () => { }, }, ], - consumer: 'bar', + consumer: 'test-consumer', name: 'abc', tags: ['foo'], enabled: true, @@ -79,7 +79,7 @@ describe('getAlertNavigationRoute', () => { getAlertNavigationRoute(router, licenseState, alertTypeRegistry, alertNavigationRegistry); const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/consumer/{consumer}/navigation"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/navigation"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -103,7 +103,7 @@ describe('getAlertNavigationRoute', () => { const [context, req, res] = mockHandlerArguments( { alertsClient }, { - params: { id: '1', consumer: 'siem' }, + params: { id: '1' }, }, ['ok'] ); @@ -113,7 +113,7 @@ describe('getAlertNavigationRoute', () => { expect(alertsClient.get.mock.calls[0][0].id).toEqual('1'); expect(alertTypeRegistry.get).toHaveBeenCalledWith('testAlertType'); - expect(alertNavigationRegistry.get).toHaveBeenCalledWith('siem', mockedAlertType); + expect(alertNavigationRegistry.get).toHaveBeenCalledWith('test-consumer', mockedAlertType); expect(res.ok).toHaveBeenCalledWith({ body: { @@ -131,7 +131,7 @@ describe('getAlertNavigationRoute', () => { getAlertNavigationRoute(router, licenseState, alertTypeRegistry, alertNavigationRegistry); const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/consumer/{consumer}/navigation"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/navigation"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -153,7 +153,7 @@ describe('getAlertNavigationRoute', () => { const [context, req, res] = mockHandlerArguments( { alertsClient }, { - params: { id: '1', consumer: 'siem' }, + params: { id: '1' }, }, ['ok'] ); @@ -163,7 +163,7 @@ describe('getAlertNavigationRoute', () => { expect(alertsClient.get.mock.calls[0][0].id).toEqual('1'); expect(alertTypeRegistry.get).toHaveBeenCalledWith('testAlertType'); - expect(alertNavigationRegistry.get).toHaveBeenCalledWith('siem', mockedAlertType); + expect(alertNavigationRegistry.get).toHaveBeenCalledWith('test-consumer', mockedAlertType); expect(res.ok).toHaveBeenCalledWith({ body: { diff --git a/x-pack/plugins/alerting/server/routes/navigation.ts b/x-pack/plugins/alerting/server/routes/navigation.ts index 08906e1261163..5b02b96004b8d 100644 --- a/x-pack/plugins/alerting/server/routes/navigation.ts +++ b/x-pack/plugins/alerting/server/routes/navigation.ts @@ -19,7 +19,6 @@ import { AlertTypeRegistry } from '../alert_type_registry'; const paramSchema = schema.object({ id: schema.string(), - consumer: schema.string(), }); export const getAlertNavigationRoute = ( @@ -30,7 +29,7 @@ export const getAlertNavigationRoute = ( ) => { router.get( { - path: `/api/alert/{id}/consumer/{consumer}/navigation`, + path: `/api/alert/{id}/navigation`, validate: { params: paramSchema, }, @@ -44,11 +43,11 @@ export const getAlertNavigationRoute = ( res: KibanaResponseFactory ): Promise> { verifyApiAccess(licenseState); - const { id, consumer } = req.params; + const { id } = req.params; const alertsClient = context.alerting.getAlertsClient(); const alert = await alertsClient.get({ id }); const alertType = alertTypeRegistry.get(alert.alertTypeId); - const navigationHandler = alertNavigationRegistry.get(consumer, alertType); + const navigationHandler = alertNavigationRegistry.get(alert.consumer, alertType); const state = navigationHandler(alert, alertType); return res.ok({ body: typeof state === 'string' ? { url: state } : { state }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/navigation.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/navigation.ts index f68d8c5577bd7..8f90abfd93f2f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/navigation.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/navigation.ts @@ -57,11 +57,7 @@ export default function alertNavigationTests({ getService }: FtrProviderContext) objectRemover.add(space.id, createdAlert.id, 'alert'); const response = await supertestWithoutAuth - .get( - `${getUrlPrefix(space.id)}/api/alert/${ - createdAlert.id - }/consumer/${consumer}/navigation` - ) + .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/navigation`) .auth(user.username, user.password); switch (scenario.id) { @@ -96,11 +92,7 @@ export default function alertNavigationTests({ getService }: FtrProviderContext) objectRemover.add(space.id, createdAlert.id, 'alert'); const response = await supertestWithoutAuth - .get( - `${getUrlPrefix('other')}/api/alert/${ - createdAlert.id - }/consumer/${consumer}/navigation` - ) + .get(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}/navigation`) .auth(user.username, user.password); expect(response.statusCode).to.eql(404); @@ -129,7 +121,7 @@ export default function alertNavigationTests({ getService }: FtrProviderContext) it(`should handle get alert request appropriately when alert doesn't exist`, async () => { const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/1/consumer/${consumer}/navigation`) + .get(`${getUrlPrefix(space.id)}/api/alert/1/navigation`) .auth(user.username, user.password); switch (scenario.id) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/navigation.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/navigation.ts index ee30d83604f31..893d13616a70a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/navigation.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/navigation.ts @@ -33,9 +33,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alert/${ - createdAlert.id - }/consumer/${consumer}/navigation` + `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/navigation` ); expect(response.statusCode).to.eql(200); @@ -53,11 +51,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); await supertest - .get( - `${getUrlPrefix(Spaces.other.id)}/api/alert/${ - createdAlert.id - }/consumer/${consumer}/navigation` - ) + .get(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}/navigation`) .expect(404, { statusCode: 404, error: 'Not Found', @@ -66,13 +60,11 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); it(`should handle get alert navigation request appropriately when alert doesn't exist`, async () => { - await supertest - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/1/consumer/${consumer}/navigation`) - .expect(404, { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [alert/1] not found', - }); + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/1/navigation`).expect(404, { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); }); }); } From ed5946c5d457fe87b0c000d2aeca6e5007fe2f8c Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 3 Mar 2020 12:24:21 +1300 Subject: [PATCH 05/45] cleaned up types --- .../alert_navigation_registry.ts | 19 ++---------- .../server/alert_navigation_registry/index.ts | 8 +++++ .../server/alert_navigation_registry/types.ts | 30 +++++++++++++++++++ x-pack/plugins/alerting/server/plugin.ts | 5 +--- .../alerting/server/routes/navigation.ts | 2 +- 5 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/alerting/server/alert_navigation_registry/index.ts create mode 100644 x-pack/plugins/alerting/server/alert_navigation_registry/types.ts diff --git a/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts b/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts index ba069d8834d3c..70253773f7c17 100644 --- a/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts +++ b/x-pack/plugins/alerting/server/alert_navigation_registry/alert_navigation_registry.ts @@ -6,23 +6,8 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; -import { AlertType, SanitizedAlert } from '../types'; -import { AlertInstances } from '../alert_instance/alert_instance'; - -interface AlertNavigationContext { - filter?: string; - dateRange?: { - start: Date; - end: Date; - }; -} - -export type AlertNavigationHandler = ( - alert: SanitizedAlert, - alertType: AlertType, - alertInstances?: AlertInstances[], - context?: AlertNavigationContext -) => Record | string; +import { AlertType } from '../types'; +import { AlertNavigationHandler } from './types'; export class AlertNavigationRegistry { private readonly alertNavigations: Map> = new Map(); diff --git a/x-pack/plugins/alerting/server/alert_navigation_registry/index.ts b/x-pack/plugins/alerting/server/alert_navigation_registry/index.ts new file mode 100644 index 0000000000000..1d8b3ffce6bcf --- /dev/null +++ b/x-pack/plugins/alerting/server/alert_navigation_registry/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './types'; +export * from './alert_navigation_registry'; diff --git a/x-pack/plugins/alerting/server/alert_navigation_registry/types.ts b/x-pack/plugins/alerting/server/alert_navigation_registry/types.ts new file mode 100644 index 0000000000000..e20500c891f4b --- /dev/null +++ b/x-pack/plugins/alerting/server/alert_navigation_registry/types.ts @@ -0,0 +1,30 @@ +/* + * 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 t from 'io-ts'; +import { JsonObject } from '../../../infra/common/typed_json'; +import { DateFromString } from '../../common/date_from_string'; +import { AlertType, SanitizedAlert } from '../types'; +import { AlertInstance } from '../alert_instance/alert_instance'; + +const dateRangechema = t.type({ + start: DateFromString, + end: DateFromString, +}); + +export const alertNavigationContextSchema = t.type({ + filter: t.string, + dateRange: dateRangechema, +}); + +export type AlertNavigationContext = t.TypeOf; + +export type AlertNavigationHandler = ( + alert: SanitizedAlert, + alertType: AlertType, + alertInstances?: AlertInstance[], + context?: AlertNavigationContext +) => JsonObject | string; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 1b2a8cd805aad..049090d058e50 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -51,10 +51,7 @@ import { PluginStartContract as ActionsPluginStartContract, } from '../../../plugins/actions/server'; import { Services } from './types'; -import { - AlertNavigationRegistry, - AlertNavigationHandler, -} from './alert_navigation_registry/alert_navigation_registry'; +import { AlertNavigationRegistry, AlertNavigationHandler } from './alert_navigation_registry'; export interface PluginSetupContract { registerType: AlertTypeRegistry['register']; diff --git a/x-pack/plugins/alerting/server/routes/navigation.ts b/x-pack/plugins/alerting/server/routes/navigation.ts index 5b02b96004b8d..9462a6db9a502 100644 --- a/x-pack/plugins/alerting/server/routes/navigation.ts +++ b/x-pack/plugins/alerting/server/routes/navigation.ts @@ -14,7 +14,7 @@ import { } from 'kibana/server'; import { LicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; -import { AlertNavigationRegistry } from '../alert_navigation_registry/alert_navigation_registry'; +import { AlertNavigationRegistry } from '../alert_navigation_registry'; import { AlertTypeRegistry } from '../alert_type_registry'; const paramSchema = schema.object({ From 2b49b7104711c1badc0c606f520e56be3f1ff07e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 3 Mar 2020 15:49:53 +1300 Subject: [PATCH 06/45] plug in View in App button on details page --- .../alerting/common/alert_navigation.ts | 14 +++ x-pack/plugins/alerting/common/index.ts | 1 + .../server/alert_navigation_registry/types.ts | 19 +--- .../alerting/server/routes/navigation.ts | 17 ++-- .../public/application/app.tsx | 2 + .../public/application/lib/alert_api.test.ts | 14 +++ .../public/application/lib/alert_api.ts | 12 ++- .../components/alert_details.tsx | 8 +- .../alert_details/components/view_in_app.tsx | 96 +++++++++++++++++++ .../with_bulk_alert_api_operations.tsx | 4 + .../triggers_actions_ui/public/plugin.ts | 6 +- .../spaces_only/tests/alerting/navigation.ts | 21 +++- 12 files changed, 179 insertions(+), 35 deletions(-) create mode 100644 x-pack/plugins/alerting/common/alert_navigation.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx diff --git a/x-pack/plugins/alerting/common/alert_navigation.ts b/x-pack/plugins/alerting/common/alert_navigation.ts new file mode 100644 index 0000000000000..fd17fa5959a03 --- /dev/null +++ b/x-pack/plugins/alerting/common/alert_navigation.ts @@ -0,0 +1,14 @@ +/* + * 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 { JsonObject } from '../../infra/common/typed_json'; +export interface AlertUrlNavigation { + url: string; +} +export interface AlertStateNavigation { + state: JsonObject; +} +export type AlertNavigation = AlertUrlNavigation | AlertStateNavigation; diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index 8c6969cded85a..5865321db09e0 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -7,6 +7,7 @@ export * from './alert'; export * from './alert_instance'; export * from './alert_task_instance'; +export * from './alert_navigation'; export interface ActionGroup { id: string; diff --git a/x-pack/plugins/alerting/server/alert_navigation_registry/types.ts b/x-pack/plugins/alerting/server/alert_navigation_registry/types.ts index e20500c891f4b..70d567aa6307b 100644 --- a/x-pack/plugins/alerting/server/alert_navigation_registry/types.ts +++ b/x-pack/plugins/alerting/server/alert_navigation_registry/types.ts @@ -4,27 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as t from 'io-ts'; import { JsonObject } from '../../../infra/common/typed_json'; -import { DateFromString } from '../../common/date_from_string'; import { AlertType, SanitizedAlert } from '../types'; -import { AlertInstance } from '../alert_instance/alert_instance'; - -const dateRangechema = t.type({ - start: DateFromString, - end: DateFromString, -}); - -export const alertNavigationContextSchema = t.type({ - filter: t.string, - dateRange: dateRangechema, -}); - -export type AlertNavigationContext = t.TypeOf; export type AlertNavigationHandler = ( alert: SanitizedAlert, - alertType: AlertType, - alertInstances?: AlertInstance[], - context?: AlertNavigationContext + alertType: AlertType ) => JsonObject | string; diff --git a/x-pack/plugins/alerting/server/routes/navigation.ts b/x-pack/plugins/alerting/server/routes/navigation.ts index 9462a6db9a502..22ddc160b465d 100644 --- a/x-pack/plugins/alerting/server/routes/navigation.ts +++ b/x-pack/plugins/alerting/server/routes/navigation.ts @@ -16,6 +16,7 @@ import { LicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { AlertNavigationRegistry } from '../alert_navigation_registry'; import { AlertTypeRegistry } from '../alert_type_registry'; +import { AlertNavigation } from '../../common'; const paramSchema = schema.object({ id: schema.string(), @@ -41,17 +42,21 @@ export const getAlertNavigationRoute = ( context: RequestHandlerContext, req: KibanaRequest, any, any, any>, res: KibanaResponseFactory - ): Promise> { + ): Promise> { verifyApiAccess(licenseState); const { id } = req.params; const alertsClient = context.alerting.getAlertsClient(); const alert = await alertsClient.get({ id }); const alertType = alertTypeRegistry.get(alert.alertTypeId); - const navigationHandler = alertNavigationRegistry.get(alert.consumer, alertType); - const state = navigationHandler(alert, alertType); - return res.ok({ - body: typeof state === 'string' ? { url: state } : { state }, - }); + if (alertNavigationRegistry.has(alert.consumer, alertType)) { + const navigationHandler = alertNavigationRegistry.get(alert.consumer, alertType); + const state = navigationHandler(alert, alertType); + return res.custom({ + statusCode: 200, + body: typeof state === 'string' ? { url: state } : { state }, + }); + } + return res.noContent(); }) ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 51ed3c1ebafad..13a6690aa6993 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -13,6 +13,7 @@ import { IUiSettingsClient, ApplicationStart, ChromeBreadcrumb, + CoreStart, } from 'kibana/public'; import { BASE_PATH, Section, routeToAlertDetails } from './constants'; import { TriggersActionsUIHome } from './home'; @@ -28,6 +29,7 @@ export interface AppDeps { dataPlugin: DataPublicPluginStart; charts: ChartsPluginStart; chrome: ChromeStart; + navigateToApp: CoreStart['application']['navigateToApp']; docLinks: DocLinksStart; toastNotifications: ToastsSetup; injectedMetadata: any; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index 1e53e7d983848..98325af69351d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -15,6 +15,7 @@ import { disableAlert, enableAlert, loadAlert, + loadAlertNavigation, loadAlerts, loadAlertState, loadAlertTypes, @@ -80,6 +81,19 @@ describe('loadAlert', () => { }); }); +describe('loadAlertNavigation', () => { + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + url: 'about:blank', + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlertNavigation({ http, alertId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/navigation`); + }); +}); + describe('loadAlertState', () => { test('should call get API with base parameters', async () => { const alertId = uuid.v4(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts index e0ecae976146c..9d5336eb7599c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts @@ -8,7 +8,7 @@ import { HttpSetup } from 'kibana/public'; import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; -import { alertStateSchema } from '../../../../alerting/common'; +import { alertStateSchema, AlertNavigation } from '../../../../alerting/common'; import { BASE_ALERT_API_PATH } from '../constants'; import { Alert, AlertType, AlertWithoutId, AlertTaskState } from '../../types'; @@ -26,6 +26,16 @@ export async function loadAlert({ return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`); } +export async function loadAlertNavigation({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise { + return await http.get(`${BASE_ALERT_API_PATH}/${alertId}/navigation`); +} + type EmptyHttpResponse = ''; export async function loadAlertState({ http, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 1952e35c22924..5dbe9ed0b299f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -33,6 +33,7 @@ import { withBulkAlertOperations, } from '../../common/components/with_bulk_alert_api_operations'; import { AlertInstancesRouteWithApi } from './alert_instances_route'; +import { ViewInAppWithApi as ViewInApp } from './view_in_app'; type AlertDetailsProps = { alert: Alert; @@ -95,12 +96,7 @@ export const AlertDetails: React.FunctionComponent = ({ - - - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx new file mode 100644 index 0000000000000..cc1aaf82aa376 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx @@ -0,0 +1,96 @@ +/* + * 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, useEffect } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CoreStart } from 'kibana/public'; +import { useAppDependencies } from '../../../app_context'; + +import { + ComponentOpts as BulkOperationsComponentOpts, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; +import { + AlertNavigation, + AlertStateNavigation, + AlertUrlNavigation, +} from '../../../../../../alerting/common'; +import { Alert } from '../../../../types'; + +type ViewInAppProps = { + alert: Alert; +} & Pick; + +const NO_NAVIGATION = 'NO_NAVIGATION'; + +type AlertNavigationLoadingState = AlertNavigation | 'NO_NAVIGATION' | null; + +export const ViewInApp: React.FunctionComponent = ({ + alert, + loadAlertNavigation, +}) => { + const { navigateToApp } = useAppDependencies(); + + const [alertNavigation, setAlertNavigation] = useState(null); + useEffect(() => { + loadAlertNavigation(alert.id) + .then(nav => (nav ? setAlertNavigation(nav) : setAlertNavigation(NO_NAVIGATION))) + .catch(() => { + setAlertNavigation(NO_NAVIGATION); + }); + }, [alert.id, loadAlertNavigation]); + + return ( + + + + ); +}; + +function hasNavigation(alertNavigation: AlertNavigationLoadingState) { + return hasNavigationState(alertNavigation) || hasNavigationUrl(alertNavigation); +} + +function hasNavigationState( + alertNavigation: AlertNavigationLoadingState +): alertNavigation is AlertStateNavigation { + return alertNavigation ? alertNavigation.hasOwnProperty('state') : false; +} + +function hasNavigationUrl( + alertNavigation: AlertNavigationLoadingState +): alertNavigation is AlertUrlNavigation { + return alertNavigation ? alertNavigation.hasOwnProperty('url') : false; +} + +function getNavigationHandler( + alertNavigation: AlertNavigationLoadingState, + alert: Alert, + navigateToApp: CoreStart['application']['navigateToApp'] +): object { + if (hasNavigationState(alertNavigation)) { + return { + onClick: () => { + navigateToApp(alert.consumer, { state: alertNavigation.state }); + }, + }; + } + if (hasNavigationUrl(alertNavigation)) { + return { href: alertNavigation.url }; + } + return {}; +} + +export const ViewInAppWithApi = withBulkAlertOperations(ViewInApp); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx index 4b348b85fe5bc..b6dea5fe6e393 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import { AlertNavigation } from '../../../../../../alerting/common'; import { Alert, AlertType, AlertTaskState } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { @@ -24,6 +25,7 @@ import { loadAlert, loadAlertState, loadAlertTypes, + loadAlertNavigation, } from '../../../lib/alert_api'; export interface ComponentOpts { @@ -41,6 +43,7 @@ export interface ComponentOpts { deleteAlert: (alert: Alert) => Promise; loadAlert: (id: Alert['id']) => Promise; loadAlertState: (id: Alert['id']) => Promise; + loadAlertNavigation: (id: Alert['id']) => Promise; loadAlertTypes: () => Promise; } @@ -105,6 +108,7 @@ export function withBulkAlertOperations( deleteAlert={async (alert: Alert) => deleteAlert({ http, id: alert.id })} loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })} loadAlertState={async (alertId: Alert['id']) => loadAlertState({ http, alertId })} + loadAlertNavigation={async (alertId: Alert['id']) => loadAlertNavigation({ http, alertId })} loadAlertTypes={async () => loadAlertTypes({ http })} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 459197d80d7aa..e0c7f520a2db4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -28,6 +28,7 @@ interface PluginsStart { data: DataPublicPluginStart; charts: ChartsPluginStart; management: ManagementStart; + navigateToApp: CoreStart['application']['navigateToApp']; } export class Plugin implements CorePlugin { @@ -58,7 +59,7 @@ export class Plugin implements CorePlugin { const objectRemover = new ObjectRemover(supertest); - const consumer = 'consumer.noop'; afterEach(() => objectRemover.removeAll()); @@ -26,7 +25,6 @@ export default function createGetTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.noop', - consumer, }) ) .expect(200); @@ -42,6 +40,25 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); }); + it('should handle get alert navigation request when there is no navigation appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.always-firing', + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/navigation` + ); + + expect(response.statusCode).to.eql(204); + }); + it(`shouldn't get navigation for an alert from another space`, async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) From 9ff010f56eca8663cf3b27c4b0b459942af70f5e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 4 Mar 2020 09:49:19 +1300 Subject: [PATCH 07/45] fixed deps in tests --- .../connector_add_flyout.test.tsx | 3 ++- .../connector_add_modal.test.tsx | 3 ++- .../components/actions_connectors_list.test.tsx | 12 ++++++++---- .../sections/alert_add/alert_add.test.tsx | 3 ++- .../sections/alert_add/alert_form.test.tsx | 3 ++- .../alerts_list/components/alerts_list.test.tsx | 12 ++++++++---- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index 6b87002a1d2cf..fb41e23fb574d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -26,7 +26,7 @@ describe('connector_add_flyout', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mocks.getStartServices(); deps = { @@ -38,6 +38,7 @@ describe('connector_add_flyout', () => { injectedMetadata: mocks.injectedMetadata, http: mocks.http, uiSettings: mocks.uiSettings, + navigateToApp, capabilities: { ...capabilities, actions: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index d9f3e98919d76..770338feac9b1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -24,7 +24,7 @@ describe('connector_add_modal', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mocks.getStartServices(); deps = { @@ -36,6 +36,7 @@ describe('connector_add_modal', () => { injectedMetadata: mocks.injectedMetadata, http: mocks.http, uiSettings: mocks.uiSettings, + navigateToApp, capabilities: { ...capabilities, actions: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 509bd7131394e..ecc0eb93cff2c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -49,7 +49,7 @@ describe('actions_connectors_list component empty', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -61,6 +61,7 @@ describe('actions_connectors_list component empty', () => { injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -146,7 +147,7 @@ describe('actions_connectors_list component with items', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -158,6 +159,7 @@ describe('actions_connectors_list component with items', () => { injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -230,7 +232,7 @@ describe('actions_connectors_list component empty with show only capability', () { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -242,6 +244,7 @@ describe('actions_connectors_list component empty with show only capability', () injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -319,7 +322,7 @@ describe('actions_connectors_list with show only capability', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -331,6 +334,7 @@ describe('actions_connectors_list with show only capability', () => { injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx index 05adccf982b7f..beb26e17b3916 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx @@ -29,7 +29,7 @@ describe('alert_add', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); deps = { @@ -41,6 +41,7 @@ describe('alert_add', () => { uiSettings: mockes.uiSettings, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + navigateToApp, capabilities: { ...capabilities, alerting: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx index aa71621f1a914..1098e9d5ce75e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.test.tsx @@ -50,7 +50,7 @@ describe('alert_form', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); deps = { @@ -62,6 +62,7 @@ describe('alert_form', () => { uiSettings: mockes.uiSettings, dataPlugin: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + navigateToApp, capabilities: { ...capabilities, siem: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 9bdad54f03352..20e367a93d604 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -83,7 +83,7 @@ describe('alerts_list component empty', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -101,6 +101,7 @@ describe('alerts_list component empty', () => { } as any, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -208,7 +209,7 @@ describe('alerts_list component with items', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -226,6 +227,7 @@ describe('alerts_list component with items', () => { } as any, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -295,7 +297,7 @@ describe('alerts_list component empty with show only capability', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -313,6 +315,7 @@ describe('alerts_list component empty with show only capability', () => { } as any, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { @@ -417,7 +420,7 @@ describe('alerts_list with show only capability', () => { { chrome, docLinks, - application: { capabilities }, + application: { capabilities, navigateToApp }, }, ] = await mockes.getStartServices(); const deps = { @@ -435,6 +438,7 @@ describe('alerts_list with show only capability', () => { } as any, http: mockes.http, uiSettings: mockes.uiSettings, + navigateToApp, capabilities: { ...capabilities, siem: { From f7033302b71d01de151caac8d3eba03361027f39 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 4 Mar 2020 14:14:03 +1300 Subject: [PATCH 08/45] added basic example plugin using alerting and navigation --- examples/alerting_example/README.md | 5 + examples/alerting_example/common/constants.ts | 20 +++ examples/alerting_example/kibana.json | 10 ++ examples/alerting_example/package.json | 17 +++ .../alerting_example/public/application.tsx | 142 ++++++++++++++++++ .../alerting_example/public/create_alert.tsx | 115 ++++++++++++++ .../alerting_example/public/documentation.tsx | 62 ++++++++ examples/alerting_example/public/index.ts | 22 +++ examples/alerting_example/public/page.tsx | 51 +++++++ examples/alerting_example/public/plugin.tsx | 41 +++++ .../alerting_example/public/view_alert.tsx | 63 ++++++++ examples/alerting_example/server/index.ts | 25 +++ examples/alerting_example/server/plugin.ts | 78 ++++++++++ examples/alerting_example/tsconfig.json | 15 ++ 14 files changed, 666 insertions(+) create mode 100644 examples/alerting_example/README.md create mode 100644 examples/alerting_example/common/constants.ts create mode 100644 examples/alerting_example/kibana.json create mode 100644 examples/alerting_example/package.json create mode 100644 examples/alerting_example/public/application.tsx create mode 100644 examples/alerting_example/public/create_alert.tsx create mode 100644 examples/alerting_example/public/documentation.tsx create mode 100644 examples/alerting_example/public/index.ts create mode 100644 examples/alerting_example/public/page.tsx create mode 100644 examples/alerting_example/public/plugin.tsx create mode 100644 examples/alerting_example/public/view_alert.tsx create mode 100644 examples/alerting_example/server/index.ts create mode 100644 examples/alerting_example/server/plugin.ts create mode 100644 examples/alerting_example/tsconfig.json diff --git a/examples/alerting_example/README.md b/examples/alerting_example/README.md new file mode 100644 index 0000000000000..bf963c64586d3 --- /dev/null +++ b/examples/alerting_example/README.md @@ -0,0 +1,5 @@ +## Alerting Example + +This example plugin shows you how to create a custom Alert Type, create alerts based on that type and corresponding UI for viewing the details of all the alerts within the custom plugin. + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/alerting_example/common/constants.ts b/examples/alerting_example/common/constants.ts new file mode 100644 index 0000000000000..9a35893646a65 --- /dev/null +++ b/examples/alerting_example/common/constants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; diff --git a/examples/alerting_example/kibana.json b/examples/alerting_example/kibana.json new file mode 100644 index 0000000000000..8ed4dfffde1b9 --- /dev/null +++ b/examples/alerting_example/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "alertingExample", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["alerting_example"], + "server": true, + "ui": true, + "requiredPlugins": ["alerting"], + "optionalPlugins": [] +} diff --git a/examples/alerting_example/package.json b/examples/alerting_example/package.json new file mode 100644 index 0000000000000..96187d847c1c4 --- /dev/null +++ b/examples/alerting_example/package.json @@ -0,0 +1,17 @@ +{ + "name": "alerting_example", + "version": "1.0.0", + "main": "target/examples/alerting_example", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + } +} diff --git a/examples/alerting_example/public/application.tsx b/examples/alerting_example/public/application.tsx new file mode 100644 index 0000000000000..9653c7290d627 --- /dev/null +++ b/examples/alerting_example/public/application.tsx @@ -0,0 +1,142 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from 'react-router-dom'; + +import { + EuiPage, + EuiPageSideBar, + // @ts-ignore + EuiSideNav, +} from '@elastic/eui'; + +import { JsonObject } from '../../../src/plugins/kibana_utils/common'; +import { AppMountParameters, CoreStart, ScopedHistory } from '../../../src/core/public'; +import { Page } from './page'; +import { DocumentationPage } from './documentation'; +import { CreateAlertPage } from './create_alert'; +import { ViewAlertPage } from './view_alert'; + +interface PageDef { + title: string; + id: string; + component: React.ReactNode; +} + +type NavProps = RouteComponentProps & { + pages: PageDef[]; +}; + +const Nav = withRouter(({ history, pages }: NavProps) => { + const navItems = pages.map(page => ({ + id: page.id, + name: page.title, + onClick: () => history.push(`/${page.id}`), + 'data-test-subj': page.id, + })); + + return ( + + ); +}); + +export interface NavState { + alert: JsonObject; + alertType: JsonObject; +} +export interface AlertingExampleComponentParams { + application: CoreStart['application']; + http: CoreStart['http']; + history: ScopedHistory; + basename: string; +} + +const Home = withRouter(({ history }) => { + return history.location.state ? : ; +}); + +const AlertingExampleApp = ({ basename, http, history }: AlertingExampleComponentParams) => { + const pages: PageDef[] = [ + { + id: 'home', + title: 'Home', + component: , + }, + { + id: 'create', + title: 'Create', + component: , + }, + { + id: 'view', + title: 'View Alert', + component: , + }, + ]; + + const routes = pages.map((page, i) => ( + { + return {page.component}; + }} + /> + )); + + return ( + + + +