From e58110a9aef251165d70e00bc2620878873c37d1 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 5 Dec 2019 16:07:56 -0500 Subject: [PATCH 01/35] [Uptime] Migrate Uptime server routing to new platform (#51125) * Move a REST endpoint and the GQL endpoint to NP routing. * Delete obsolete REST endpoint. * Update remaining REST routes to work with NP router. * Remove obsolete code, update some unit tests. * Simplify route creation. * Remove tests of API decommissioned API endpoint. * Rename domain check. * Make return shape of index pattern endpoint correspond to required NP resp body. * Move validate to appropriate level of route definition object for monitor details endpoint. * Update snapshot count route. * Fix broken lint rule. * Remove usages of Boom. * Fix license router creation. --- x-pack/legacy/plugins/uptime/index.ts | 2 +- .../auth/__tests__/xpack_auth_adapter.test.ts | 56 ------------- .../server/lib/adapters/auth/adapter_types.ts | 32 -------- .../uptime/server/lib/adapters/auth/index.ts | 8 -- .../lib/adapters/auth/xpack_auth_adapter.ts | 40 --------- .../lib/adapters/framework/adapter_types.ts | 45 +++------- .../framework/kibana_framework_adapter.ts | 82 +++++++++++-------- .../uptime/server/lib/adapters/index.ts | 1 - .../uptime/server/lib/compose/kibana.ts | 9 +- .../__tests__/__snapshots__/auth.test.ts.snap | 9 -- .../__snapshots__/license.test.ts.snap | 28 +++++++ .../server/lib/domains/__tests__/auth.test.ts | 59 ------------- .../lib/domains/__tests__/license.test.ts | 40 +++++++++ .../plugins/uptime/server/lib/domains/auth.ts | 40 --------- .../uptime/server/lib/domains/index.ts | 2 +- .../uptime/server/lib/domains/license.ts | 39 +++++++++ .../legacy/plugins/uptime/server/lib/lib.ts | 4 +- .../uptime/server/rest_api/auth/index.ts | 7 -- .../uptime/server/rest_api/auth/is_valid.ts | 16 ---- .../server/rest_api/create_route_with_auth.ts | 22 +++-- .../plugins/uptime/server/rest_api/index.ts | 2 - .../index_pattern/get_index_pattern.ts | 20 ++++- .../rest_api/monitors/monitors_details.ts | 24 +++--- .../uptime/server/rest_api/pings/get_all.ts | 43 ++++++---- .../rest_api/snapshot/get_snapshot_count.ts | 31 ++++--- .../rest_api/telemetry/log_monitor_page.ts | 8 +- .../rest_api/telemetry/log_overview_page.ts | 8 +- .../plugins/uptime/server/rest_api/types.ts | 11 ++- .../apis/uptime/feature_controls.ts | 26 ------ 29 files changed, 278 insertions(+), 436 deletions(-) delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/auth/__tests__/xpack_auth_adapter.test.ts delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/auth/adapter_types.ts delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/auth/index.ts delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/auth/xpack_auth_adapter.ts delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/auth.test.ts.snap create mode 100644 x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/auth.test.ts create mode 100644 x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/license.test.ts delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/domains/auth.ts create mode 100644 x-pack/legacy/plugins/uptime/server/lib/domains/license.ts delete mode 100644 x-pack/legacy/plugins/uptime/server/rest_api/auth/index.ts delete mode 100644 x-pack/legacy/plugins/uptime/server/rest_api/auth/is_valid.ts diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts index 690807cc91e27..3cd0ffb1a2942 100644 --- a/x-pack/legacy/plugins/uptime/index.ts +++ b/x-pack/legacy/plugins/uptime/index.ts @@ -41,7 +41,7 @@ export const uptime = (kibana: any) => plugin(initializerContext).setup( { - route: (arg: any) => server.route(arg), + route: server.newPlatform.setup.core.http.createRouter(), }, { elasticsearch, diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/__tests__/xpack_auth_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/__tests__/xpack_auth_adapter.test.ts deleted file mode 100644 index 7dff7a02e0dc7..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/__tests__/xpack_auth_adapter.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 { UMAuthContainer, UMXPackLicenseStatus } from '../adapter_types'; -import { UMXPackAuthAdapter } from '../xpack_auth_adapter'; - -const setupXPack = (licenseStatus: UMXPackLicenseStatus) => ({ - info: { - feature: (pluginId: string) => ({ - registerLicenseCheckResultsGenerator: ( - licenseCheckResultsHandler: (status: UMXPackLicenseStatus) => void - ) => { - licenseCheckResultsHandler(licenseStatus); - }, - }), - }, - status: { - once: (status: string, registerLicenseCheck: () => void) => { - registerLicenseCheck(); - }, - }, -}); - -describe('X-PackAuthAdapter class', () => { - let xpack: UMAuthContainer; - - beforeEach(() => { - xpack = setupXPack({ - license: { - isActive: () => true, - getType: () => 'platinum', - }, - }); - }); - - it('returns the license type', () => { - const adapter = new UMXPackAuthAdapter(xpack); - expect(adapter.getLicenseType()).toBe('platinum'); - expect(adapter.licenseIsActive()).toBe(true); - }); - - it('returns null and false for undefined license values', () => { - xpack = setupXPack({ - license: { - getType: () => undefined, - isActive: () => undefined, - }, - }); - const adapter = new UMXPackAuthAdapter(xpack); - expect(adapter.licenseIsActive()).toBe(false); - expect(adapter.getLicenseType()).toBeNull(); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/adapter_types.ts deleted file mode 100644 index 016e623d7745d..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/adapter_types.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 interface UMAuthContainer { - info: { - feature: ( - pluginId: string - ) => { - registerLicenseCheckResultsGenerator: ( - licenseCheckResultsHandler: (info: UMXPackLicenseStatus) => void - ) => void; - }; - }; - status: { - once: (status: string, registerLicenseCheck: () => void) => void; - }; -} - -export interface UMXPackLicenseStatus { - license: { - isActive: () => boolean | undefined; - getType: () => string | undefined; - }; -} - -export interface UMAuthAdapter { - getLicenseType(): string | null; - licenseIsActive(): boolean; -} diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/index.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/index.ts deleted file mode 100644 index baa31ef752daa..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * 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 { UMAuthAdapter, UMAuthContainer } from './adapter_types'; -export { UMXPackAuthAdapter } from './xpack_auth_adapter'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/xpack_auth_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/xpack_auth_adapter.ts deleted file mode 100644 index 3393cfd5fa758..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/auth/xpack_auth_adapter.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { PLUGIN } from '../../../../common/constants'; -import { UMAuthAdapter, UMAuthContainer, UMXPackLicenseStatus } from './adapter_types'; - -// look at index-management for guidance, subscribe to licensecheckerresultsgenerator -// then check the license status -export class UMXPackAuthAdapter implements UMAuthAdapter { - private xpackLicenseStatus: { - isActive: boolean | undefined | null; - licenseType: string | undefined | null; - }; - - constructor(private readonly xpack: UMAuthContainer) { - this.xpack = xpack; - this.xpackLicenseStatus = { - isActive: null, - licenseType: null, - }; - this.xpack.status.once('green', this.registerLicenseCheck); - } - - public getLicenseType = (): string | null => this.xpackLicenseStatus.licenseType || null; - - public licenseIsActive = (): boolean => this.xpackLicenseStatus.isActive || false; - - private registerLicenseCheck = (): void => - this.xpack.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(this.updateLicenseInfo); - - private updateLicenseInfo = (xpackLicenseStatus: UMXPackLicenseStatus): void => { - this.xpackLicenseStatus = { - isActive: xpackLicenseStatus.license.isActive(), - licenseType: xpackLicenseStatus.license.getType(), - }; - }; -} diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index df2723283f88c..e67f4fa59531c 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -4,35 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GraphQLOptions } from 'apollo-server-core'; import { GraphQLSchema } from 'graphql'; -import { Lifecycle, ResponseToolkit } from 'hapi'; -import { RouteOptions } from 'hapi'; -import { SavedObjectsLegacyService } from 'src/core/server'; +import { SavedObjectsLegacyService, RequestHandler, IRouter } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; - -export interface UMFrameworkRequest { - user: string; - headers: Record; - payload: Record; - params: Record; - query: Record; -} - -export type UMFrameworkResponse = Lifecycle.ReturnValue; +import { ObjectType } from '@kbn/config-schema'; +import { UMRouteDefinition } from '../../../rest_api'; export interface UMFrameworkRouteOptions< - RouteRequest extends UMFrameworkRequest, - RouteResponse extends UMFrameworkResponse + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType > { path: string; method: string; - handler: (req: Request, h: ResponseToolkit) => any; + handler: RequestHandler; config?: any; + validate: any; } export interface UptimeCoreSetup { - route: any; + route: IRouter; } export interface UptimeCorePlugins { @@ -42,24 +33,8 @@ export interface UptimeCorePlugins { xpack: any; } -export type UMFrameworkRouteHandler = ( - request: UMFrameworkRequest, - h: ResponseToolkit -) => void; - -export type HapiOptionsFunction = (req: Request) => GraphQLOptions | Promise; - -export interface UMHapiGraphQLPluginOptions { - path: string; - vhost?: string; - route?: RouteOptions; - graphQLOptions: GraphQLOptions | HapiOptionsFunction; -} - export interface UMBackendFrameworkAdapter { - registerRoute( - route: UMFrameworkRouteOptions - ): void; + registerRoute(route: UMRouteDefinition): void; registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; getSavedObjectsClient(): any; } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts index c166530ca6428..a46a7e11bd738 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -5,16 +5,11 @@ */ import { GraphQLSchema } from 'graphql'; -import { Request, ResponseToolkit } from 'hapi'; +import { schema as kbnSchema } from '@kbn/config-schema'; import { runHttpQuery } from 'apollo-server-core'; import { UptimeCorePlugins, UptimeCoreSetup } from './adapter_types'; -import { - UMBackendFrameworkAdapter, - UMFrameworkRequest, - UMFrameworkResponse, - UMFrameworkRouteOptions, -} from './adapter_types'; -import { DEFAULT_GRAPHQL_PATH } from '../../../graphql'; +import { UMBackendFrameworkAdapter } from './adapter_types'; +import { UMRouteDefinition } from '../../../rest_api'; export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapter { constructor( @@ -25,11 +20,22 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte this.plugins = plugins; } - public registerRoute< - RouteRequest extends UMFrameworkRequest, - RouteResponse extends UMFrameworkResponse - >(route: UMFrameworkRouteOptions) { - this.server.route(route); + public registerRoute({ handler, method, options, path, validate }: UMRouteDefinition) { + const routeDefinition = { + path, + validate, + options, + }; + switch (method) { + case 'GET': + this.server.route.get(routeDefinition, handler); + break; + case 'POST': + this.server.route.post(routeDefinition, handler); + break; + default: + throw new Error(`Handler for method ${method} is not defined`); + } } public registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void { @@ -43,37 +49,47 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte tags: ['access:uptime'], }, }; - this.server.route({ - options: options.route, - handler: async (request: Request, h: ResponseToolkit) => { + this.server.route.post( + { + path: routePath, + validate: { + body: kbnSchema.object({ + operationName: kbnSchema.nullable(kbnSchema.string()), + query: kbnSchema.string(), + variables: kbnSchema.recordOf(kbnSchema.string(), kbnSchema.any()), + }), + }, + options: { + tags: ['access:uptime'], + }, + }, + async (context, request, resp): Promise => { try { - const { method } = request; - const query = - method === 'post' - ? (request.payload as Record) - : (request.query as Record); + const query = request.body as Record; const graphQLResponse = await runHttpQuery([request], { - method: method.toUpperCase(), + method: 'POST', options: options.graphQLOptions, query, }); - return h.response(graphQLResponse).type('application/json'); + return resp.ok({ + body: graphQLResponse, + headers: { + 'content-type': 'application/json', + }, + }); } catch (error) { if (error.isGraphQLError === true) { - return h - .response(error.message) - .code(error.statusCode) - .type('application/json'); + return resp.internalError({ + body: { message: error.message }, + headers: { 'content-type': 'application/json' }, + }); } - return h.response(error).type('application/json'); + return resp.internalError(); } - }, - method: ['get', 'post'], - path: options.path || DEFAULT_GRAPHQL_PATH, - vhost: undefined, - }); + } + ); } public getSavedObjectsClient() { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts index a09f80ae3b40a..2a88df91adc29 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './auth'; export * from './database'; export * from './framework'; export * from './monitor_states'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts index 1c9999fbf6451..a7c370e03490b 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts @@ -4,26 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UMXPackAuthAdapter } from '../adapters/auth'; import { UMKibanaDatabaseAdapter } from '../adapters/database/kibana_database_adapter'; import { UMKibanaBackendFrameworkAdapter } from '../adapters/framework'; import { ElasticsearchMonitorsAdapter } from '../adapters/monitors'; import { ElasticsearchPingsAdapter } from '../adapters/pings'; -import { UMAuthDomain } from '../domains'; +import { licenseCheck } from '../domains'; import { UMDomainLibs, UMServerLibs } from '../lib'; import { ElasticsearchMonitorStatesAdapter } from '../adapters/monitor_states'; import { UMKibanaSavedObjectsAdapter } from '../adapters/saved_objects/kibana_saved_objects_adapter'; import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters/framework'; export function compose(server: UptimeCoreSetup, plugins: UptimeCorePlugins): UMServerLibs { - const { elasticsearch, savedObjects, xpack } = plugins; + const { elasticsearch, savedObjects } = plugins; const framework = new UMKibanaBackendFrameworkAdapter(server, plugins); const database = new UMKibanaDatabaseAdapter(elasticsearch); - const authDomain = new UMAuthDomain(new UMXPackAuthAdapter(xpack), {}); - const domainLibs: UMDomainLibs = { - auth: authDomain, + license: licenseCheck, monitors: new ElasticsearchMonitorsAdapter(database), monitorStates: new ElasticsearchMonitorStatesAdapter(database), pings: new ElasticsearchPingsAdapter(database), diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/auth.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/auth.test.ts.snap deleted file mode 100644 index 3a959d5af7933..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/auth.test.ts.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Auth domain lib throws for inactive license 1`] = `"this.adapter.getLicenseType is not a function"`; - -exports[`Auth domain lib throws for null license 1`] = `"Missing license information"`; - -exports[`Auth domain lib throws for unsupported license type 1`] = `"License not supported"`; - -exports[`Auth domain lib throws if request is not authenticated 1`] = `"Missing authentication"`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap new file mode 100644 index 0000000000000..ab791cbd243f8 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`license check returns result for a valid license 1`] = ` +Object { + "statusCode": 200, +} +`; + +exports[`license check throws for inactive license 1`] = ` +Object { + "message": "License not active", + "statusCode": 403, +} +`; + +exports[`license check throws for null license 1`] = ` +Object { + "message": "Missing license information", + "statusCode": 400, +} +`; + +exports[`license check throws for unsupported license type 1`] = ` +Object { + "message": "License not supported", + "statusCode": 401, +} +`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/auth.test.ts b/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/auth.test.ts deleted file mode 100644 index 5d9de84a6ebbc..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/auth.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 { UMAuthDomain } from '../auth'; - -describe('Auth domain lib', () => { - let domain: UMAuthDomain; - let mockAdapter: any; - let mockGetLicenseType: jest.Mock; - let mockLicenseIsActive: jest.Mock; - - beforeEach(() => { - mockAdapter = jest.fn(); - mockGetLicenseType = jest.fn(); - mockLicenseIsActive = jest.fn(); - domain = new UMAuthDomain(mockAdapter, {}); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('throws for null license', () => { - mockGetLicenseType.mockReturnValue(null); - mockAdapter.getLicenseType = mockGetLicenseType; - expect(() => domain.requestIsValid({})).toThrowErrorMatchingSnapshot(); - }); - - it('throws for unsupported license type', () => { - mockGetLicenseType.mockReturnValue('oss'); - mockAdapter.getLicenseType = mockGetLicenseType; - expect(() => domain.requestIsValid({})).toThrowErrorMatchingSnapshot(); - }); - - it('throws for inactive license', () => { - mockLicenseIsActive.mockReturnValue(false); - mockAdapter.licenseIsActive = mockLicenseIsActive; - expect(() => domain.requestIsValid({})).toThrowErrorMatchingSnapshot(); - }); - - it('throws if request is not authenticated', () => { - mockGetLicenseType.mockReturnValue('basic'); - mockLicenseIsActive.mockReturnValue(true); - mockAdapter.getLicenseType = mockGetLicenseType; - mockAdapter.licenseIsActive = mockLicenseIsActive; - expect(() => domain.requestIsValid({})).toThrowErrorMatchingSnapshot(); - }); - - it('accepts request if authenticated', () => { - mockGetLicenseType.mockReturnValue('basic'); - mockLicenseIsActive.mockReturnValue(true); - mockAdapter.getLicenseType = mockGetLicenseType; - mockAdapter.licenseIsActive = mockLicenseIsActive; - expect(domain.requestIsValid({ auth: { isAuthenticated: true } })).toEqual(true); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/license.test.ts b/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/license.test.ts new file mode 100644 index 0000000000000..b26cb99cf9b6b --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/license.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { ILicense } from '../../../../../../../plugins/licensing/server'; +import { licenseCheck } from '../license'; + +describe('license check', () => { + let mockLicense: Pick; + + it('throws for null license', () => { + expect(licenseCheck(null)).toMatchSnapshot(); + }); + + it('throws for unsupported license type', () => { + mockLicense = { + isOneOf: jest.fn().mockReturnValue(false), + isActive: false, + }; + expect(licenseCheck(mockLicense)).toMatchSnapshot(); + }); + + it('throws for inactive license', () => { + mockLicense = { + isOneOf: jest.fn().mockReturnValue(true), + isActive: false, + }; + expect(licenseCheck(mockLicense)).toMatchSnapshot(); + }); + + it('returns result for a valid license', () => { + mockLicense = { + isOneOf: jest.fn().mockReturnValue(true), + isActive: true, + }; + expect(licenseCheck(mockLicense)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/auth.ts b/x-pack/legacy/plugins/uptime/server/lib/domains/auth.ts deleted file mode 100644 index 62d17f121779f..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/domains/auth.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { get } from 'lodash'; -import { UMAuthAdapter } from '../adapters/auth/adapter_types'; - -const supportedLicenses = ['basic', 'standard', 'gold', 'platinum', 'trial']; - -export class UMAuthDomain { - constructor(private readonly adapter: UMAuthAdapter, libs: {}) { - this.adapter = adapter; - } - - public requestIsValid(request: any): boolean { - const license = this.adapter.getLicenseType(); - if (license === null) { - throw Boom.badRequest('Missing license information'); - } - if (!supportedLicenses.some(licenseType => licenseType === license)) { - throw Boom.forbidden('License not supported'); - } - if (this.adapter.licenseIsActive() === false) { - throw Boom.forbidden('License not active'); - } - - return this.checkRequest(request); - } - - private checkRequest(request: any): boolean { - const authenticated = get(request, 'auth.isAuthenticated', null); - if (authenticated === null) { - throw Boom.forbidden('Missing authentication'); - } - return authenticated; - } -} diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/index.ts b/x-pack/legacy/plugins/uptime/server/lib/domains/index.ts index 5be591708fab1..6a35b9b41020b 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/domains/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/domains/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { UMAuthDomain } from './auth'; +export { licenseCheck, UMLicenseCheck } from './license'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/license.ts b/x-pack/legacy/plugins/uptime/server/lib/domains/license.ts new file mode 100644 index 0000000000000..9c52667aeeab4 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/domains/license.ts @@ -0,0 +1,39 @@ +/* + * 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 { ILicense } from '../../../../../../plugins/licensing/server'; + +export interface UMLicenseStatusResponse { + statusCode: number; + message?: string; +} +export type UMLicenseCheck = ( + license: Pick | null +) => UMLicenseStatusResponse; + +export const licenseCheck: UMLicenseCheck = license => { + if (license === null) { + return { + message: 'Missing license information', + statusCode: 400, + }; + } + if (!license.isOneOf(['basic', 'standard', 'gold', 'platinum', 'trial'])) { + return { + message: 'License not supported', + statusCode: 401, + }; + } + if (license.isActive === false) { + return { + message: 'License not active', + statusCode: 403, + }; + } + return { + statusCode: 200, + }; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/lib.ts b/x-pack/legacy/plugins/uptime/server/lib/lib.ts index 217195488677e..e68a6dd18ef5f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/lib.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/lib.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UMAuthDomain } from './domains'; import { DatabaseAdapter, UMBackendFrameworkAdapter, @@ -13,9 +12,10 @@ import { UMPingsAdapter, UMSavedObjectsAdapter, } from './adapters'; +import { UMLicenseCheck } from './domains'; export interface UMDomainLibs { - auth: UMAuthDomain; + license: UMLicenseCheck; monitors: UMMonitorsAdapter; monitorStates: UMMonitorStatesAdapter; pings: UMPingsAdapter; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/auth/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/auth/index.ts deleted file mode 100644 index 242d9bf73a946..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/rest_api/auth/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * 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 { createIsValidRoute } from './is_valid'; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/auth/is_valid.ts b/x-pack/legacy/plugins/uptime/server/rest_api/auth/is_valid.ts deleted file mode 100644 index 15024f7c5f497..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/rest_api/auth/is_valid.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 { UMServerLibs } from '../../lib/lib'; - -export const createIsValidRoute = (libs: UMServerLibs) => ({ - method: 'GET', - path: '/api/uptime/is_valid', - handler: async (request: any): Promise => await libs.auth.requestIsValid(request), - options: { - tags: ['access:uptime'], - }, -}); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/create_route_with_auth.ts b/x-pack/legacy/plugins/uptime/server/rest_api/create_route_with_auth.ts index 727ed78624a48..a1976ef046e8e 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/create_route_with_auth.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/create_route_with_auth.ts @@ -4,26 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; +import { RequestHandler } from 'kibana/server'; +import { ObjectType } from '@kbn/config-schema'; import { UMServerLibs } from '../lib/lib'; -import { UMRestApiRouteCreator, UMServerRoute } from './types'; +import { UMRestApiRouteCreator, UMRouteDefinition } from './types'; export const createRouteWithAuth = ( libs: UMServerLibs, routeCreator: UMRestApiRouteCreator -): UMServerRoute => { +): UMRouteDefinition => { const restRoute = routeCreator(libs); - const { handler, method, path, options } = restRoute; - const authHandler = async (request: any, h: any) => { - if (libs.auth.requestIsValid(request)) { - return await handler(request, h); + const { handler, method, path, options, ...rest } = restRoute; + const authHandler: RequestHandler = async ( + context, + request, + response + ) => { + if (libs.license(context.licensing.license)) { + return await handler(context, request, response); } - return Boom.badRequest(); + return response.badRequest(); }; return { method, path, options, handler: authHandler, + ...rest, }; }; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts index 889f8a820b2f3..2810982fb0c6c 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createIsValidRoute } from './auth'; import { createGetAllRoute } from './pings'; import { createGetIndexPatternRoute } from './index_pattern'; import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry'; @@ -19,7 +18,6 @@ export const restApiRoutes: UMRestApiRouteCreator[] = [ createGetIndexPatternRoute, createGetMonitorDetailsRoute, createGetSnapshotCount, - createIsValidRoute, createLogMonitorPageRoute, createLogOverviewPageRoute, ]; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts b/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts index 3cdd71fda3e43..55f0af57ed847 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts @@ -5,12 +5,24 @@ */ import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteCreator } from '../types'; -export const createGetIndexPatternRoute = (libs: UMServerLibs) => ({ +export const createGetIndexPatternRoute: UMRestApiRouteCreator = (libs: UMServerLibs) => ({ method: 'GET', path: '/api/uptime/index_pattern', - tags: ['access:uptime'], - handler: async (): Promise => { - return await libs.savedObjects.getUptimeIndexPattern(); + validate: false, + options: { + tags: ['access:uptime'], + }, + handler: async (_context, _request, response): Promise => { + try { + return response.ok({ + body: { + ...(await libs.savedObjects.getUptimeIndexPattern()), + }, + }); + } catch (e) { + return response.internalError({ body: { message: e.message } }); + } }, }); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts index 1440b55c1c137..00860248ff153 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -3,23 +3,27 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; + +import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; -import { MonitorDetails } from '../../../common/runtime_types/monitor/monitor_details'; +import { UMRestApiRouteCreator } from '../types'; -export const createGetMonitorDetailsRoute = (libs: UMServerLibs) => ({ +export const createGetMonitorDetailsRoute: UMRestApiRouteCreator = (libs: UMServerLibs) => ({ method: 'GET', path: '/api/uptime/monitor/details', + validate: { + query: schema.object({ + monitorId: schema.maybe(schema.string()), + }), + }, options: { - validate: { - query: Joi.object({ - monitorId: Joi.string(), - }), - }, tags: ['access:uptime'], }, - handler: async (request: any): Promise => { + handler: async (_context, request, response): Promise => { const { monitorId } = request.query; - return await libs.monitors.getMonitorDetails(request, monitorId); + + return response.ok({ + body: { ...(await libs.monitors.getMonitorDetails(request, monitorId)) }, + }); }, }); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_all.ts b/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_all.ts index 70f9a5c061191..e2bdbca9fbfa0 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_all.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_all.ts @@ -4,36 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; -import { PingResults } from '../../../common/graphql/types'; +import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteCreator } from '../types'; -export const createGetAllRoute = (libs: UMServerLibs) => ({ +export const createGetAllRoute: UMRestApiRouteCreator = (libs: UMServerLibs) => ({ method: 'GET', path: '/api/uptime/pings', + validate: { + query: schema.object({ + dateRangeStart: schema.string(), + dateRangeEnd: schema.string(), + location: schema.maybe(schema.string()), + monitorId: schema.maybe(schema.string()), + size: schema.maybe(schema.number()), + sort: schema.maybe(schema.string()), + status: schema.maybe(schema.string()), + }), + }, options: { - validate: { - query: Joi.object({ - dateRangeStart: Joi.number().required(), - dateRangeEnd: Joi.number().required(), - monitorId: Joi.string(), - size: Joi.number(), - sort: Joi.string(), - status: Joi.string(), - }), - }, tags: ['access:uptime'], }, - handler: async (request: any): Promise => { - const { size, sort, dateRangeStart, dateRangeEnd, monitorId, status } = request.query; - return await libs.pings.getAll( + handler: async (_context, request, response): Promise => { + const { size, sort, dateRangeStart, dateRangeEnd, location, monitorId, status } = request.query; + + const result = await libs.pings.getAll( request, dateRangeStart, dateRangeEnd, monitorId, status, sort, - size + size, + location ); + + return response.ok({ + body: { + ...result, + }, + }); }, }); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index ddca622005d63..9b2b26f52699e 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -4,32 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; +import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; -import { Snapshot } from '../../../common/runtime_types'; +import { UMRestApiRouteCreator } from '../types'; -export const createGetSnapshotCount = (libs: UMServerLibs) => ({ +export const createGetSnapshotCount: UMRestApiRouteCreator = (libs: UMServerLibs) => ({ method: 'GET', path: '/api/uptime/snapshot/count', + validate: { + query: schema.object({ + dateRangeStart: schema.string(), + dateRangeEnd: schema.string(), + filters: schema.maybe(schema.string()), + statusFilter: schema.maybe(schema.string()), + }), + }, options: { - validate: { - query: Joi.object({ - dateRangeStart: Joi.string().required(), - dateRangeEnd: Joi.string().required(), - filters: Joi.string(), - statusFilter: Joi.string(), - }), - }, tags: ['access:uptime'], }, - handler: async (request: any): Promise => { + handler: async (_context, request, response): Promise => { const { dateRangeStart, dateRangeEnd, filters, statusFilter } = request.query; - return await libs.monitorStates.getSnapshotCount( + const result = await libs.monitorStates.getSnapshotCount( request, dateRangeStart, dateRangeEnd, filters, statusFilter ); + return response.ok({ + body: { + ...result, + }, + }); }, }); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts b/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts index 34c689be6e521..f3e926493143b 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts @@ -5,13 +5,15 @@ */ import { KibanaTelemetryAdapter } from '../../lib/adapters/telemetry'; +import { UMRestApiRouteCreator } from '../types'; -export const createLogMonitorPageRoute = () => ({ +export const createLogMonitorPageRoute: UMRestApiRouteCreator = () => ({ method: 'POST', path: '/api/uptime/logMonitor', - handler: async (request: any, h: any): Promise => { + validate: false, + handler: async (_context, _request, response): Promise => { await KibanaTelemetryAdapter.countMonitor(); - return h.response().code(200); + return response.ok(); }, options: { tags: ['access:uptime'], diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts b/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts index bd7e7d85637e7..277ef2235fb69 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts @@ -5,13 +5,15 @@ */ import { KibanaTelemetryAdapter } from '../../lib/adapters/telemetry'; +import { UMRestApiRouteCreator } from '../types'; -export const createLogOverviewPageRoute = () => ({ +export const createLogOverviewPageRoute: UMRestApiRouteCreator = () => ({ method: 'POST', path: '/api/uptime/logOverview', - handler: async (request: any, h: any): Promise => { + validate: false, + handler: async (_context, _request, response): Promise => { await KibanaTelemetryAdapter.countOverview(); - return h.response().code(200); + return response.ok(); }, options: { tags: ['access:uptime'], diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/types.ts b/x-pack/legacy/plugins/uptime/server/rest_api/types.ts index acbc50828f874..b4d7e438f51be 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/types.ts @@ -4,13 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ObjectType } from '@kbn/config-schema'; +import { RequestHandler, RouteConfig, RouteMethod } from 'kibana/server'; import { UMServerLibs } from '../lib/lib'; export interface UMServerRoute { method: string; - path: string; - options?: any; - handler: (request: any, h?: any) => any; + handler: RequestHandler; } -export type UMRestApiRouteCreator = (libs: UMServerLibs) => UMServerRoute; +export type UMRouteDefinition = UMServerRoute & + RouteConfig; + +export type UMRestApiRouteCreator = (libs: UMServerLibs) => UMRouteDefinition; diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts index a0161c0beb945..adbfacb014e9f 100644 --- a/x-pack/test/api_integration/apis/uptime/feature_controls.ts +++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts @@ -46,17 +46,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) .catch((error: any) => ({ error, response: undefined })); }; - const executeIsValidRequest = async (username: string, password: string, spaceId?: string) => { - const basePath = spaceId ? `/s/${spaceId}` : ''; - - return await supertest - .get(`${basePath}/api/uptime/is_valid`) - .auth(username, password) - .set('kbn-xsrf', 'foo') - .then((response: any) => ({ error: undefined, response })) - .catch((error: any) => ({ error, response: undefined })); - }; - const executePingsRequest = async (username: string, password: string, spaceId?: string) => { const basePath = spaceId ? `/s/${spaceId}` : ''; @@ -96,9 +85,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const graphQLResult = await executeGraphQLQuery(username, password); expect404(graphQLResult); - const isValidResult = await executeIsValidRequest(username, password); - expect404(isValidResult); - const pingsResult = await executePingsRequest(username, password); expect404(pingsResult); } finally { @@ -138,9 +124,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const graphQLResult = await executeGraphQLQuery(username, password); expectResponse(graphQLResult); - const isValidResult = await executeIsValidRequest(username, password); - expectResponse(isValidResult); - const pingsResult = await executePingsRequest(username, password); expectResponse(pingsResult); } finally { @@ -183,9 +166,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const graphQLResult = await executeGraphQLQuery(username, password); expect404(graphQLResult); - const isValidResult = await executeIsValidRequest(username, password); - expect404(isValidResult); - const pingsResult = await executePingsRequest(username, password); expect404(pingsResult); } finally { @@ -255,9 +235,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const graphQLResult = await executeGraphQLQuery(username, password, space1Id); expectResponse(graphQLResult); - const isValidResult = await executeIsValidRequest(username, password, space1Id); - expectResponse(isValidResult); - const pingsResult = await executePingsRequest(username, password, space1Id); expectResponse(pingsResult); }); @@ -266,9 +243,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const graphQLResult = await executeGraphQLQuery(username, password); expect404(graphQLResult); - const isValidResult = await executeIsValidRequest(username, password); - expect404(isValidResult); - const pingsResult = await executePingsRequest(username, password); expect404(pingsResult); }); From 84bba66e1d6c1e265e3d2ff36526134cd8f0b765 Mon Sep 17 00:00:00 2001 From: Dan Roscigno Date: Thu, 5 Dec 2019 20:01:04 -0500 Subject: [PATCH 02/35] Add tutorial for using RBAC with Spaces (#52197) * add spaces with RBAC tutorial * add tutorial * removed extra dir * revert * init tutorial * link tutorial * fix images dir * fixed links * editing * Update docs/user/security/rbac_tutorial.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/user/security/rbac_tutorial.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/user/security/rbac_tutorial.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/user/security/rbac_tutorial.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/user/security/rbac_tutorial.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * show path * added path to crate user * rearrange * remove image * specify admin role * replace we with you * Update docs/user/security/rbac_tutorial.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * remove excess words * remove excess words * spelling * spelling * Update docs/user/security/rbac_tutorial.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/user/security/rbac_tutorial.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * resolve reviewer comments * review updates * address review comments * removed monitor cluster priv * reviewer comments --- .../security/images/role-index-privilege.png | Bin 0 -> 80869 bytes docs/user/security/images/role-management.png | Bin 0 -> 187928 bytes docs/user/security/images/role-new-user.png | Bin 0 -> 134706 bytes .../images/role-space-visualization.png | Bin 0 -> 98970 bytes docs/user/security/index.asciidoc | 2 +- docs/user/security/rbac_tutorial.asciidoc | 104 ++++++++++++++++++ 6 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 docs/user/security/images/role-index-privilege.png create mode 100644 docs/user/security/images/role-management.png create mode 100644 docs/user/security/images/role-new-user.png create mode 100644 docs/user/security/images/role-space-visualization.png create mode 100644 docs/user/security/rbac_tutorial.asciidoc diff --git a/docs/user/security/images/role-index-privilege.png b/docs/user/security/images/role-index-privilege.png new file mode 100644 index 0000000000000000000000000000000000000000..1dc1ae640e3ba362816d3240e4f7ed074415ae7b GIT binary patch literal 80869 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≫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 literal 0 HcmV?d00001 diff --git a/docs/user/security/images/role-management.png b/docs/user/security/images/role-management.png new file mode 100644 index 0000000000000000000000000000000000000000..2a78c69a5e3528b6e645de11a9a7c209a31b6529 GIT binary patch literal 187928 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

W9R3|LCYnz=scHAf^SVY#{?Q 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

ZwRm_i{I>w!|`0UEYvb)D%L5}Kxs0w=o4r@?C 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 `; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.tsx index a7308514de182..98acb1cfa8045 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.tsx @@ -228,6 +228,7 @@ export const CustomUrlEditor: FC = ({ onChange={onLabelChange} isInvalid={isInvalidLabel} compressed + data-test-subj="mlJobCustomUrlLabelInput" /> = ({ job, customUrls, setCust : []; return ( - + = ({ job, customUrls, setCust value={label} isInvalid={isInvalidLabel} onChange={e => onLabelChange(e, index)} + data-test-subj={`mlJobEditCustomUrlLabelInput_${index}`} /> @@ -266,5 +267,5 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust ); }); - return <>{customUrlRows}; + return
{customUrlRows}
; }; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx index ed2c880c1b65f..c36b4ceed7d57 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx @@ -223,7 +223,11 @@ export class CustomUrls extends Component { : true; const addButton = ( - + { ) : ( - + { <> {(!editorOpen || editMode === 'modal') && ( - + = ({ additionalExpanded, setAdditional buttonContent={ButtonContent} onToggle={setAdditionalExpanded} initialIsOpen={additionalExpanded} + data-test-subj="mlJobWizardToggleAdditionalSettingsSection" > - +
+ - - - - - + + + + + - - - - - - + + + + + + +
); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx index c441d1fe6270c..919972186761a 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx @@ -67,7 +67,7 @@ export const CalendarsSelection: FC = () => { - + { await esArchiver.load('ml/ecommerce'); + await ml.api.createCalendar('wizard-test-calendar'); }); after(async () => { @@ -464,6 +465,18 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertJobGroupSelection(testData.jobGroups); }); + it('job creation opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job creation adds a new custom url', async () => { + await ml.jobWizardCommon.addCustomUrl({ label: 'check-kibana-dashboard' }); + }); + + it('job creation assigns calendars', async () => { + await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + }); + it('job creation displays the model plot switch', async () => { await ml.jobWizardCommon.assertModelPlotSwitchExists({ withAdvancedSection: false }); }); @@ -709,6 +722,18 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertJobGroupSelection(testData.jobGroupsClone); }); + it('job cloning opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job cloning persists custom urls', async () => { + await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + }); + + it('job cloning persists assigned calendars', async () => { + await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + }); + it('job cloning pre-fills the model plot switch', async () => { await ml.jobWizardCommon.assertModelPlotSwitchExists({ withAdvancedSection: false }); await ml.jobWizardCommon.assertModelPlotSwitchCheckedState(false, { diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts index 935fbc0102149..d41d96e40e2be 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts @@ -75,6 +75,7 @@ export default function({ getService }: FtrProviderContext) { this.tags(['smoke', 'mlqa']); before(async () => { await esArchiver.load('ml/farequote'); + await ml.api.createCalendar('wizard-test-calendar'); }); after(async () => { @@ -170,6 +171,18 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertJobGroupSelection(jobGroups); }); + it('job creation opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job creation adds a new custom url', async () => { + await ml.jobWizardCommon.addCustomUrl({ label: 'check-kibana-dashboard' }); + }); + + it('job creation assigns calendars', async () => { + await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + }); + it('job creation opens the advanced section', async () => { await ml.jobWizardCommon.ensureAdvancedSectionOpen(); }); @@ -306,6 +319,18 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertJobGroupSelection(jobGroupsClone); }); + it('job cloning opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job cloning persists custom urls', async () => { + await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + }); + + it('job cloning persists assigned calendars', async () => { + await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + }); + it('job cloning opens the advanced section', async () => { await ml.jobWizardCommon.ensureAdvancedSectionOpen(); }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts index ff2275837ce2e..296af3179ce3e 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts @@ -89,6 +89,7 @@ export default function({ getService }: FtrProviderContext) { this.tags(['smoke', 'mlqa']); before(async () => { await esArchiver.load('ml/ecommerce'); + await ml.api.createCalendar('wizard-test-calendar'); }); after(async () => { @@ -197,6 +198,18 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertJobGroupSelection(jobGroups); }); + it('job creation opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job creation adds a new custom url', async () => { + await ml.jobWizardCommon.addCustomUrl({ label: 'check-kibana-dashboard' }); + }); + + it('job creation assigns calendars', async () => { + await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + }); + it('job creation opens the advanced section', async () => { await ml.jobWizardCommon.ensureAdvancedSectionOpen(); }); @@ -344,6 +357,18 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertJobGroupSelection(jobGroupsClone); }); + it('job cloning opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job cloning persists custom urls', async () => { + await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + }); + + it('job cloning persists assigned calendars', async () => { + await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + }); + it('job cloning opens the advanced section', async () => { await ml.jobWizardCommon.ensureAdvancedSectionOpen(); }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts index 1983e98a0123d..f6cd7b40bc7b1 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts @@ -74,6 +74,7 @@ export default function({ getService }: FtrProviderContext) { this.tags(['smoke', 'mlqa']); before(async () => { await esArchiver.load('ml/farequote'); + await ml.api.createCalendar('wizard-test-calendar'); }); after(async () => { @@ -151,6 +152,18 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertJobGroupSelection(jobGroups); }); + it('job creation opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job creation adds a new custom url', async () => { + await ml.jobWizardCommon.addCustomUrl({ label: 'check-kibana-dashboard' }); + }); + + it('job creation assigns calendars', async () => { + await ml.jobWizardCommon.addCalendar('wizard-test-calendar'); + }); + it('job creation opens the advanced section', async () => { await ml.jobWizardCommon.ensureAdvancedSectionOpen(); }); @@ -271,6 +284,18 @@ export default function({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertJobGroupSelection(jobGroupsClone); }); + it('job cloning opens the additional settings section', async () => { + await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); + }); + + it('job cloning persists custom urls', async () => { + await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + }); + + it('job cloning persists assigned calendars', async () => { + await ml.jobWizardCommon.assertCalendarsSelection(['wizard-test-calendar']); + }); + it('job cloning opens the advanced section', async () => { await ml.jobWizardCommon.ensureAdvancedSectionOpen(); }); diff --git a/x-pack/test/functional/services/machine_learning/api.ts b/x-pack/test/functional/services/machine_learning/api.ts index be65896950cfc..1995f37782948 100644 --- a/x-pack/test/functional/services/machine_learning/api.ts +++ b/x-pack/test/functional/services/machine_learning/api.ts @@ -247,5 +247,25 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } }); }, + + async getCalendar(calendarId: string) { + return await esSupertest.get(`/_ml/calendars/${calendarId}`).expect(200); + }, + + async createCalendar(calendarId: string, body = { description: '', job_ids: [] }) { + log.debug(`Creating calendar with id '${calendarId}'...`); + await esSupertest + .put(`/_ml/calendars/${calendarId}`) + .send(body) + .expect(200); + + await retry.waitForWithTimeout(`'${calendarId}' to be created`, 30 * 1000, async () => { + if (await this.getCalendar(calendarId)) { + return true; + } else { + throw new Error(`expected calendar '${calendarId}' to be created`); + } + }); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/custom_urls.ts b/x-pack/test/functional/services/machine_learning/custom_urls.ts new file mode 100644 index 0000000000000..dc6e4a2fccb10 --- /dev/null +++ b/x-pack/test/functional/services/machine_learning/custom_urls.ts @@ -0,0 +1,45 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertCustomUrlLabelValue(expectedValue: string) { + const actualCustomUrlLabel = await testSubjects.getAttribute( + 'mlJobCustomUrlLabelInput', + 'value' + ); + expect(actualCustomUrlLabel).to.eql(expectedValue); + }, + + async setCustomUrlLabel(customUrlsLabel: string) { + await testSubjects.setValue('mlJobCustomUrlLabelInput', customUrlsLabel, { + clearWithKeyboard: true, + }); + await this.assertCustomUrlLabelValue(customUrlsLabel); + }, + + async assertCustomUrlItem(index: number, label: string) { + await testSubjects.existOrFail(`mlJobEditCustomUrlItem_${index}`); + expect( + await testSubjects.getAttribute(`mlJobEditCustomUrlLabelInput_${index}`, 'value') + ).to.eql(label); + }, + + /** + * Submits the custom url form and adds it to the list. + * @param formContainerSelector - selector for the element that wraps the custom url creation form. + */ + async saveCustomUrl(formContainerSelector: string) { + await testSubjects.click('mlJobAddCustomUrl'); + await testSubjects.missingOrFail(formContainerSelector, { timeout: 10 * 1000 }); + }, + }; +} diff --git a/x-pack/test/functional/services/machine_learning/index.ts b/x-pack/test/functional/services/machine_learning/index.ts index c62b714f566e9..adaded0832522 100644 --- a/x-pack/test/functional/services/machine_learning/index.ts +++ b/x-pack/test/functional/services/machine_learning/index.ts @@ -6,6 +6,7 @@ export { MachineLearningAnomalyExplorerProvider } from './anomaly_explorer'; export { MachineLearningAPIProvider } from './api'; +export { MachineLearningCustomUrlsProvider } from './custom_urls'; export { MachineLearningDataFrameAnalyticsProvider } from './data_frame_analytics'; export { MachineLearningDataFrameAnalyticsCreationProvider } from './data_frame_analytics_creation'; export { MachineLearningDataFrameAnalyticsTableProvider } from './data_frame_analytics_table'; diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts index 0ebc4cb959412..235e597f8c280 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts @@ -4,10 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MachineLearningCustomUrlsProvider } from './custom_urls'; -export function MachineLearningJobWizardCommonProvider({ getService }: FtrProviderContext) { +export function MachineLearningJobWizardCommonProvider( + { getService }: FtrProviderContext, + customUrls: ProvidedType +) { const comboBox = getService('comboBox'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); @@ -166,6 +171,23 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid expect(await this.getSelectedJobGroups()).to.contain(jobGroup); }, + async getSelectedCalendars(): Promise { + await this.ensureAdditionalSettingsSectionOpen(); + return await comboBox.getComboBoxSelectedOptions( + 'mlJobWizardComboBoxCalendars > comboBoxInput' + ); + }, + + async assertCalendarsSelection(calendars: string[]) { + expect(await this.getSelectedCalendars()).to.eql(calendars); + }, + + async addCalendar(calendarId: string) { + await this.ensureAdditionalSettingsSectionOpen(); + await comboBox.setCustom('mlJobWizardComboBoxCalendars > comboBoxInput', calendarId); + expect(await this.getSelectedCalendars()).to.contain(calendarId); + }, + async assertModelPlotSwitchExists( sectionOptions: SectionOptions = { withAdvancedSection: true } ) { @@ -358,6 +380,40 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid await this.assertDateRangeSelection(expectedStartDate, expectedEndDate); }, + async ensureAdditionalSettingsSectionOpen() { + await retry.tryForTime(5000, async () => { + if ((await testSubjects.exists('mlJobWizardAdditionalSettingsSection')) === false) { + await testSubjects.click('mlJobWizardToggleAdditionalSettingsSection'); + await testSubjects.existOrFail('mlJobWizardAdditionalSettingsSection', { timeout: 1000 }); + } + }); + }, + + async ensureNewCustomUrlFormModalOpen() { + await retry.tryForTime(5000, async () => { + if ((await testSubjects.exists('mlJobNewCustomUrlFormModal')) === false) { + await testSubjects.click('mlJobOpenCustomUrlFormButton'); + await testSubjects.existOrFail('mlJobNewCustomUrlFormModal', { timeout: 1000 }); + } + }); + }, + + async addCustomUrl(customUrl: { label: string }) { + await this.ensureAdditionalSettingsSectionOpen(); + + const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlsList > *'); + + await this.ensureNewCustomUrlFormModalOpen(); + // Fill-in the form + await customUrls.setCustomUrlLabel(customUrl.label); + // Save custom URL + await customUrls.saveCustomUrl('mlJobNewCustomUrlFormModal'); + + const expectedIndex = existingCustomUrls.length; + + await customUrls.assertCustomUrlItem(expectedIndex, customUrl.label); + }, + async ensureAdvancedSectionOpen() { await retry.tryForTime(5000, async () => { if ((await testSubjects.exists(advancedSectionSelector())) === false) { diff --git a/x-pack/test/functional/services/ml.ts b/x-pack/test/functional/services/ml.ts index 86967dfd1e273..4b6f77262b7f9 100644 --- a/x-pack/test/functional/services/ml.ts +++ b/x-pack/test/functional/services/ml.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; import { MachineLearningAnomalyExplorerProvider, MachineLearningAPIProvider, + MachineLearningCustomUrlsProvider, MachineLearningDataFrameAnalyticsProvider, MachineLearningDataFrameAnalyticsCreationProvider, MachineLearningDataFrameAnalyticsTableProvider, @@ -30,6 +31,7 @@ import { export function MachineLearningProvider(context: FtrProviderContext) { const anomalyExplorer = MachineLearningAnomalyExplorerProvider(context); const api = MachineLearningAPIProvider(context); + const customUrls = MachineLearningCustomUrlsProvider(context); const dataFrameAnalytics = MachineLearningDataFrameAnalyticsProvider(context, api); const dataFrameAnalyticsCreation = MachineLearningDataFrameAnalyticsCreationProvider(context); const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context); @@ -40,7 +42,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const jobTable = MachineLearningJobTableProvider(context); const jobTypeSelection = MachineLearningJobTypeSelectionProvider(context); const jobWizardAdvanced = MachineLearningJobWizardAdvancedProvider(context); - const jobWizardCommon = MachineLearningJobWizardCommonProvider(context); + const jobWizardCommon = MachineLearningJobWizardCommonProvider(context, customUrls); const jobWizardMultiMetric = MachineLearningJobWizardMultiMetricProvider(context); const jobWizardPopulation = MachineLearningJobWizardPopulationProvider(context); const navigation = MachineLearningNavigationProvider(context); @@ -50,6 +52,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { return { anomalyExplorer, api, + customUrls, dataFrameAnalytics, dataFrameAnalyticsCreation, dataFrameAnalyticsTable, From c4c95e2bc64f5a9b919eb72af26eba1bc6879190 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 6 Dec 2019 09:34:52 -0700 Subject: [PATCH 11/35] [Maps] use style metadata to calculate symbolization bands (#51713) * [Maps] use style metadata to calculate symbolization bands * only update style meta when fields change * load join source style meta * use style meta data request to populate range * apply source filter to style meta request * fix heatmap * only use style meta range if field supports field meta * add fieldMetaOptions to style prperty descriptor and add migration script * add UI for setting fieldMetaOptions.isEnabled * clean up * review feedback * fix can_skip_fetch tests * review feedback * only show field meta popover for fields that support field meta * avoid duplicate fields re-fetching style meta * clean up problems when first creating grid source * update text for enabling field meta toggle * provide UI for setting sigma * allow users to include global time in style meta request * update SIEM saved objects * add less than and greater than symbols when styling by field stats * fix functional tests * review feedback * add support for date fields * review feedback * only show less then and greater then in legend when values will be outside of std range * unnest VectorStyle._getFieldRange * remove unused function * only show style isTimeAware switch when style fields use field meta --- .../legacy/plugins/maps/common/constants.js | 11 +- .../migrations/add_field_meta_options.js | 38 +++++ .../migrations/add_field_meta_options.test.js | 121 +++++++++++++++ x-pack/legacy/plugins/maps/migrations.js | 6 +- .../maps/public/layers/fields/es_agg_field.js | 17 ++- .../public/layers/fields/es_agg_field.test.js | 29 ++++ .../maps/public/layers/fields/es_doc_field.js | 27 ++++ .../maps/public/layers/fields/field.js | 8 + .../maps/public/layers/joins/inner_join.js | 7 +- .../plugins/maps/public/layers/layer.js | 2 +- .../es_geo_grid_source/es_geo_grid_source.js | 21 ++- .../update_source_editor.js | 4 +- .../es_pew_pew_source/es_pew_pew_source.js | 19 ++- .../es_search_source/es_search_source.js | 16 +- .../maps/public/layers/sources/es_source.js | 44 +++++- .../public/layers/sources/es_term_source.js | 10 +- .../maps/public/layers/sources/source.js | 6 +- .../layers/styles/heatmap/heatmap_style.js | 3 +- .../components/field_meta_options_popover.js | 139 ++++++++++++++++++ .../components/get_vector_style_label.js | 12 +- .../legend/style_property_legend_row.js | 12 +- .../components/static_dynamic_style_row.js | 32 +++- .../vector/components/vector_style_editor.js | 46 +++++- .../properties/dynamic_color_property.js | 2 +- .../dynamic_orientation_property.js | 4 +- .../properties/dynamic_size_property.js | 8 +- .../properties/dynamic_style_property.js | 23 ++- .../public/layers/styles/vector/style_util.js | 17 ++- .../layers/styles/vector/style_util.test.js | 33 +++++ .../layers/styles/vector/vector_style.js | 125 ++++++++++++---- .../styles/vector/vector_style_defaults.js | 45 ++++-- .../maps/public/layers/util/can_skip_fetch.js | 19 +++ .../public/layers/util/can_skip_fetch.test.js | 6 +- .../maps/public/layers/util/data_request.js | 2 +- .../public/layers/util/is_metric_countable.js | 11 ++ .../maps/public/layers/vector_layer.js | 94 +++++++++++- .../components/embeddables/__mocks__/mock.ts | 4 + .../components/embeddables/map_config.ts | 4 + .../es_archives/maps/kibana/data.json | 2 +- 39 files changed, 910 insertions(+), 119 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js create mode 100644 x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index 3b2f887e13c87..77b57e3fe4965 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -57,6 +57,8 @@ export const FIELD_ORIGIN = { }; export const SOURCE_DATA_ID_ORIGIN = 'source'; +export const META_ID_ORIGIN_SUFFIX = 'meta'; +export const SOURCE_META_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${META_ID_ORIGIN_SUFFIX}`; export const GEOJSON_FILE = 'GEOJSON_FILE'; @@ -124,6 +126,11 @@ export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLab export const COUNT_PROP_NAME = 'doc_count'; export const STYLE_TYPE = { - 'STATIC': 'STATIC', - 'DYNAMIC': 'DYNAMIC' + STATIC: 'STATIC', + DYNAMIC: 'DYNAMIC' +}; + +export const LAYER_STYLE_TYPE = { + VECTOR: 'VECTOR', + HEATMAP: 'HEATMAP' }; diff --git a/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js new file mode 100644 index 0000000000000..ed585e013d06f --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js @@ -0,0 +1,38 @@ +/* + * 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 _ from 'lodash'; +import { LAYER_TYPE, STYLE_TYPE } from '../constants'; + +function isVectorLayer(layerDescriptor) { + const layerType = _.get(layerDescriptor, 'type'); + return layerType === LAYER_TYPE.VECTOR; +} + +export function addFieldMetaOptions({ attributes }) { + if (!attributes.layerListJSON) { + return attributes; + } + + const layerList = JSON.parse(attributes.layerListJSON); + layerList.forEach((layerDescriptor) => { + if (isVectorLayer(layerDescriptor) && _.has(layerDescriptor, 'style.properties')) { + Object.values(layerDescriptor.style.properties).forEach(stylePropertyDescriptor => { + if (stylePropertyDescriptor.type === STYLE_TYPE.DYNAMIC) { + stylePropertyDescriptor.options.fieldMetaOptions = { + isEnabled: false, // turn off field metadata to avoid changing behavior of existing saved objects + sigma: 3, + }; + } + }); + } + }); + + return { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }; +} diff --git a/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js new file mode 100644 index 0000000000000..905f77223b3bc --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js @@ -0,0 +1,121 @@ +/* + * 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 { addFieldMetaOptions } from './add_field_meta_options'; +import { LAYER_TYPE, STYLE_TYPE } from '../constants'; + +describe('addFieldMetaOptions', () => { + + test('Should handle missing layerListJSON attribute', () => { + const attributes = { + title: 'my map', + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + }); + }); + + test('Should ignore non-vector layers', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.HEATMAP, + style: { + type: 'HEATMAP', + colorRampName: 'Greens' + } + } + ]); + const attributes = { + title: 'my map', + layerListJSON + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + layerListJSON + }); + }); + + test('Should ignore static style properties', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + style: { + type: 'VECTOR', + properties: { + lineColor: { + type: STYLE_TYPE.STATIC, + options: { + color: '#FFFFFF' + } + } + } + } + } + ]); + const attributes = { + title: 'my map', + layerListJSON + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + layerListJSON + }); + }); + + test('Should add field meta options to dynamic style properties', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + field: { + name: 'my_field', + origin: 'source' + }, + color: 'Greys' + } + } + } + } + } + ]); + const attributes = { + title: 'my map', + layerListJSON + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + layerListJSON: JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + field: { + name: 'my_field', + origin: 'source' + }, + color: 'Greys', + fieldMetaOptions: { + isEnabled: false, + sigma: 3, + } + } + } + } + } + } + ]) + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/legacy/plugins/maps/migrations.js index 39dc58f259961..df19c8425199a 100644 --- a/x-pack/legacy/plugins/maps/migrations.js +++ b/x-pack/legacy/plugins/maps/migrations.js @@ -8,6 +8,7 @@ import { extractReferences } from './common/migrations/references'; import { emsRasterTileToEmsVectorTile } from './common/migrations/ems_raster_tile_to_ems_vector_tile'; import { topHitsTimeToSort } from './common/migrations/top_hits_time_to_sort'; import { moveApplyGlobalQueryToSources } from './common/migrations/move_apply_global_query'; +import { addFieldMetaOptions } from './common/migrations/add_field_meta_options'; export const migrations = { 'map': { @@ -37,11 +38,12 @@ export const migrations = { }; }, '7.6.0': (doc) => { - const attributes = moveApplyGlobalQueryToSources(doc); + const attributesPhase1 = moveApplyGlobalQueryToSources(doc); + const attributesPhase2 = addFieldMetaOptions({ attributes: attributesPhase1 }); return { ...doc, - attributes, + attributes: attributesPhase2, }; } }, diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js index eb80169e94eab..af78e3a871802 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ - import { AbstractField } from './field'; import { COUNT_AGG_TYPE } from '../../../common/constants'; +import { isMetricCountable } from '../util/is_metric_countable'; import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property'; export class ESAggMetricField extends AbstractField { @@ -36,6 +36,11 @@ export class ESAggMetricField extends AbstractField { return (this.getAggType() === COUNT_AGG_TYPE) ? true : !!this._esDocField; } + async getDataType() { + // aggregations only provide numerical data + return 'number'; + } + getESDocFieldName() { return this._esDocField ? this._esDocField.getName() : ''; } @@ -55,7 +60,6 @@ export class ESAggMetricField extends AbstractField { ); } - makeMetricAggConfig() { const metricAggConfig = { id: this.getName(), @@ -69,4 +73,13 @@ export class ESAggMetricField extends AbstractField { } return metricAggConfig; } + + supportsFieldMeta() { + // count and sum aggregations are not within field bounds so they do not support field meta. + return !isMetricCountable(this.getAggType()); + } + + async getFieldMetaRequest(config) { + return this._esDocField.getFieldMetaRequest(config); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js new file mode 100644 index 0000000000000..65b8c518fa895 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js @@ -0,0 +1,29 @@ +/* + * 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 { ESAggMetricField } from './es_agg_field'; +import { METRIC_TYPE } from '../../../common/constants'; + +describe('supportsFieldMeta', () => { + + test('Non-counting aggregations should support field meta', () => { + const avgMetric = new ESAggMetricField({ aggType: METRIC_TYPE.AVG }); + expect(avgMetric.supportsFieldMeta()).toBe(true); + const maxMetric = new ESAggMetricField({ aggType: METRIC_TYPE.MAX }); + expect(maxMetric.supportsFieldMeta()).toBe(true); + const minMetric = new ESAggMetricField({ aggType: METRIC_TYPE.MIN }); + expect(minMetric.supportsFieldMeta()).toBe(true); + }); + + test('Counting aggregations should not support field meta', () => { + const countMetric = new ESAggMetricField({ aggType: METRIC_TYPE.COUNT }); + expect(countMetric.supportsFieldMeta()).toBe(false); + const sumMetric = new ESAggMetricField({ aggType: METRIC_TYPE.SUM }); + expect(sumMetric.supportsFieldMeta()).toBe(false); + const uniqueCountMetric = new ESAggMetricField({ aggType: METRIC_TYPE.UNIQUE_COUNT }); + expect(uniqueCountMetric.supportsFieldMeta()).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js index 5cc0c9a29ce02..ad15c6249e554 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js @@ -27,4 +27,31 @@ export class ESDocField extends AbstractField { return field.type; } + supportsFieldMeta() { + return true; + } + + async getFieldMetaRequest(/* config */) { + const field = await this._getField(); + + if (field.type !== 'number' && field.type !== 'date') { + return null; + } + + const extendedStats = {}; + if (field.scripted) { + extendedStats.script = { + source: field.script, + lang: field.lang + }; + } else { + extendedStats.field = this._fieldName; + } + return { + [this._fieldName]: { + extended_stats: extendedStats + } + }; + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/field.js b/x-pack/legacy/plugins/maps/public/layers/fields/field.js index b53c6991c6ebe..f1bb116d29c8b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/field.js @@ -42,4 +42,12 @@ export class AbstractField { getOrigin() { return this._origin; } + + supportsFieldMeta() { + return false; + } + + async getFieldMetaRequest(/* config */) { + return null; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js index 184fdc0663bd7..432492973cce0 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js @@ -7,6 +7,7 @@ import { ESTermSource } from '../sources/es_term_source'; import { getComputedFieldNamePrefix } from '../styles/vector/style_util'; +import { META_ID_ORIGIN_SUFFIX } from '../../../common/constants'; export class InnerJoin { @@ -36,10 +37,14 @@ export class InnerJoin { // Source request id must be static and unique because the re-fetch logic uses the id to locate the previous request. // Elasticsearch sources have a static and unique id so that requests can be modified in the inspector. // Using the right source id as the source request id because it meets the above criteria. - getSourceId() { + getSourceDataRequestId() { return `join_source_${this._rightSource.getId()}`; } + getSourceMetaDataRequestId() { + return `${this.getSourceDataRequestId()}_${META_ID_ORIGIN_SUFFIX}`; + } + getLeftField() { return this._leftField; } diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 1c2f33df66bf8..b1f3c32f267b9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -80,7 +80,7 @@ export class AbstractLayer { } supportsElasticsearchFilters() { - return this._source.supportsElasticsearchFilters(); + return this._source.isESSource(); } async supportsFitToBounds() { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index 413f99480a8c2..f4cb43ad90146 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -15,7 +15,7 @@ import { AggConfigs } from 'ui/agg_types'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { convertToGeoJson } from './convert_to_geojson'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { vectorStyles } from '../../styles/vector/vector_style_defaults'; +import { getDefaultDynamicProperties, VECTOR_STYLES } from '../../styles/vector/vector_style_defaults'; import { RENDER_AS } from './render_as'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; @@ -170,13 +170,15 @@ export class ESGeoGridSource extends AbstractESAggSource { const searchSource = await this._makeSearchSource(searchFilters, 0); const aggConfigs = new AggConfigs(indexPattern, this._makeAggConfigs(searchFilters.geogridPrecision), aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); - const esResponse = await this._runEsQuery( - layerName, + const esResponse = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, searchSource, registerCancelCallback, - i18n.translate('xpack.maps.source.esGrid.inspectorDescription', { + requestDescription: i18n.translate('xpack.maps.source.esGrid.inspectorDescription', { defaultMessage: 'Elasticsearch geo grid aggregation request' - })); + }), + }); const tabifiedResp = tabifyAggResponse(aggConfigs, esResponse); const { featureCollection } = convertToGeoJson({ @@ -226,10 +228,14 @@ export class ESGeoGridSource extends AbstractESAggSource { sourceDescriptor: this._descriptor, ...options }); + + const defaultDynamicProperties = getDefaultDynamicProperties(); + descriptor.style = VectorStyle.createDescriptor({ - [vectorStyles.FILL_COLOR]: { + [VECTOR_STYLES.FILL_COLOR]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, @@ -238,9 +244,10 @@ export class ESGeoGridSource extends AbstractESAggSource { color: 'Blues' } }, - [vectorStyles.ICON_SIZE]: { + [VECTOR_STYLES.ICON_SIZE]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js index 1b446e1f2159a..cc1e53dc5cb3f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js @@ -8,13 +8,13 @@ import React, { Fragment, Component } from 'react'; import { RENDER_AS } from './render_as'; import { MetricsEditor } from '../../../components/metrics_editor'; -import { METRIC_TYPE } from '../../../../common/constants'; import { indexPatternService } from '../../../kibana_services'; import { ResolutionEditor } from './resolution_editor'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; +import { isMetricCountable } from '../../util/is_metric_countable'; export class UpdateSourceEditor extends Component { state = { @@ -72,7 +72,7 @@ export class UpdateSourceEditor extends Component { this.props.renderAs === RENDER_AS.HEATMAP ? metric => { //these are countable metrics, where blending heatmap color blobs make sense - return [METRIC_TYPE.COUNT, METRIC_TYPE.SUM, METRIC_TYPE.UNIQUE_COUNT].includes(metric.value); + return isMetricCountable(metric.value); } : null; const allowMultipleMetrics = this.props.renderAs !== RENDER_AS.HEATMAP; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index 01220136b14f3..4eb0a952defba 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -12,7 +12,7 @@ import { VectorLayer } from '../../vector_layer'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { vectorStyles } from '../../styles/vector/vector_style_defaults'; +import { getDefaultDynamicProperties, VECTOR_STYLES } from '../../styles/vector/vector_style_defaults'; import { i18n } from '@kbn/i18n'; import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW, COUNT_PROP_NAME, COUNT_PROP_LABEL } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -123,10 +123,12 @@ export class ESPewPewSource extends AbstractESAggSource { } createDefaultLayer(options) { + const defaultDynamicProperties = getDefaultDynamicProperties(); const styleDescriptor = VectorStyle.createDescriptor({ - [vectorStyles.LINE_COLOR]: { + [VECTOR_STYLES.LINE_COLOR]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, @@ -135,9 +137,10 @@ export class ESPewPewSource extends AbstractESAggSource { color: 'Blues' } }, - [vectorStyles.LINE_WIDTH]: { + [VECTOR_STYLES.LINE_WIDTH]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, @@ -203,13 +206,15 @@ export class ESPewPewSource extends AbstractESAggSource { } }); - const esResponse = await this._runEsQuery( - layerName, + const esResponse = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, searchSource, registerCancelCallback, - i18n.translate('xpack.maps.source.pewPew.inspectorDescription', { + requestDescription: i18n.translate('xpack.maps.source.pewPew.inspectorDescription', { defaultMessage: 'Source-destination connections request' - })); + }), + }); const { featureCollection } = convertToLines(esResponse); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 57a43f924b7e6..453a1851e47aa 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -261,7 +261,13 @@ export class ESSearchSource extends AbstractESSource { } }); - const resp = await this._runEsQuery(layerName, searchSource, registerCancelCallback, 'Elasticsearch document top hits request'); + const resp = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, + searchSource, + registerCancelCallback, + requestDescription: 'Elasticsearch document top hits request', + }); const allHits = []; const entityBuckets = _.get(resp, 'aggregations.entitySplit.buckets', []); @@ -322,7 +328,13 @@ export class ESSearchSource extends AbstractESSource { searchSource.setField('sort', this._buildEsSort()); } - const resp = await this._runEsQuery(layerName, searchSource, registerCancelCallback, 'Elasticsearch document request'); + const resp = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, + searchSource, + registerCancelCallback, + requestDescription: 'Elasticsearch document request', + }); return { hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index c2f4f7e755288..b5d7f7a6f606a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -54,7 +54,7 @@ export class AbstractESSource extends AbstractVectorSource { return []; } - supportsElasticsearchFilters() { + isESSource() { return true; } @@ -73,7 +73,7 @@ export class AbstractESSource extends AbstractVectorSource { return []; } - async _runEsQuery(requestName, searchSource, registerCancelCallback, requestDescription) { + async _runEsQuery({ requestId, requestName, requestDescription, searchSource, registerCancelCallback }) { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); @@ -82,7 +82,7 @@ export class AbstractESSource extends AbstractVectorSource { inspectorAdapters: this._inspectorAdapters, searchSource, requestName, - requestId: this.getId(), + requestId, requestDesc: requestDescription, abortSignal: abortController.signal, }); @@ -271,4 +271,42 @@ export class AbstractESSource extends AbstractVectorSource { return fieldFromIndexPattern.format.getConverterFor('text'); } + async loadStylePropsMeta(layerName, style, dynamicStyleProps, registerCancelCallback, searchFilters) { + const promises = dynamicStyleProps.map(dynamicStyleProp => { + return dynamicStyleProp.getFieldMetaRequest(); + }); + + const fieldAggRequests = await Promise.all(promises); + const aggs = fieldAggRequests.reduce((aggs, fieldAggRequest) => { + return fieldAggRequest ? { ...aggs, ...fieldAggRequest } : aggs; + }, {}); + + const indexPattern = await this.getIndexPattern(); + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('size', 0); + searchSource.setField('aggs', aggs); + if (searchFilters.sourceQuery) { + searchSource.setField('query', searchFilters.sourceQuery); + } + if (style.isTimeAware() && await this.isTimeAware()) { + searchSource.setField('filter', [timefilter.createFilter(indexPattern, searchFilters.timeFilters)]); + } + + const resp = await this._runEsQuery({ + requestId: `${this.getId()}_styleMeta`, + requestName: i18n.translate('xpack.maps.source.esSource.stylePropsMetaRequestName', { + defaultMessage: '{layerName} - metadata', + values: { layerName } + }), + searchSource, + registerCancelCallback, + requestDescription: i18n.translate('xpack.maps.source.esSource.stylePropsMetaRequestDescription', { + defaultMessage: 'Elasticsearch request retrieving field metadata used for calculating symbolization bands.', + }), + }); + + return resp.aggregations; + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js index afc402fa81bcb..57366e502d581 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js @@ -103,9 +103,13 @@ export class ESTermSource extends AbstractESAggSource { const aggConfigs = new AggConfigs(indexPattern, configStates, aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); - const requestName = `${this._descriptor.indexPatternTitle}.${this._termField.getName()}`; - const requestDesc = this._getRequestDescription(leftSourceName, leftFieldName); - const rawEsData = await this._runEsQuery(requestName, searchSource, registerCancelCallback, requestDesc); + const rawEsData = await this._runEsQuery({ + requestId: this.getId(), + requestName: `${this._descriptor.indexPatternTitle}.${this._termField.getName()}`, + searchSource, + registerCancelCallback, + requestDescription: this._getRequestDescription(leftSourceName, leftFieldName), + }); const metricPropertyNames = configStates .filter(configState => { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.js b/x-pack/legacy/plugins/maps/public/layers/sources/source.js index 78e57f79bbe56..d3b2971dbbb0c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.js @@ -123,7 +123,7 @@ export class AbstractSource { return AbstractSource.isIndexingSource; } - supportsElasticsearchFilters() { + isESSource() { return false; } @@ -136,6 +136,10 @@ export class AbstractSource { async getFieldFormatter(/* fieldName */) { return null; } + + async loadStylePropsMeta() { + throw new Error(`Source#loadStylePropsMeta not implemented`); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js index e4982c86b53bb..ed64f408b2585 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js @@ -10,13 +10,14 @@ import { AbstractStyle } from '../abstract_style'; import { HeatmapStyleEditor } from './components/heatmap_style_editor'; import { HeatmapLegend } from './components/legend/heatmap_legend'; import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants'; +import { LAYER_STYLE_TYPE } from '../../../../common/constants'; import { getColorRampStops } from '../color_utils'; import { i18n } from '@kbn/i18n'; import { EuiIcon } from '@elastic/eui'; export class HeatmapStyle extends AbstractStyle { - static type = 'HEATMAP'; + static type = LAYER_STYLE_TYPE.HEATMAP; constructor(descriptor = {}) { super(); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js new file mode 100644 index 0000000000000..095740abe3dda --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js @@ -0,0 +1,139 @@ +/* + * 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, { Component, Fragment } from 'react'; +import { + EuiButtonIcon, + EuiFormRow, + EuiPopover, + EuiRange, + EuiSwitch, +} from '@elastic/eui'; +import { VECTOR_STYLES } from '../vector_style_defaults'; +import { i18n } from '@kbn/i18n'; + +function getIsEnableToggleLabel(styleName) { + switch (styleName) { + case VECTOR_STYLES.FILL_COLOR: + case VECTOR_STYLES.LINE_COLOR: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.colorLabel', { + defaultMessage: 'Calculate color ramp range from indices' + }); + case VECTOR_STYLES.LINE_WIDTH: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.widthLabel', { + defaultMessage: 'Calculate border width range from indices' + }); + case VECTOR_STYLES.ICON_SIZE: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.sizeLabel', { + defaultMessage: 'Calculate symbol size range from indices' + }); + default: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.defaultLabel', { + defaultMessage: 'Calculate symbolization range from indices' + }); + } +} + +export class FieldMetaOptionsPopover extends Component { + + state = { + isPopoverOpen: false, + }; + + _togglePopover = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + } + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + } + + _onIsEnabledChange = event => { + this.props.onChange({ + ...this.props.styleProperty.getFieldMetaOptions(), + isEnabled: event.target.checked, + }); + }; + + _onSigmaChange = event => { + this.props.onChange({ + ...this.props.styleProperty.getFieldMetaOptions(), + sigma: event.target.value, + }); + } + + _renderButton() { + return ( + + ); + } + + _renderContent() { + return ( + + + + + + + + + + ); + } + + render() { + if (!this.props.styleProperty.supportsFieldMeta()) { + return null; + } + + return ( + + {this._renderContent()} + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js index 0984b0189558d..b21577d214bb5 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js @@ -6,27 +6,27 @@ import { i18n } from '@kbn/i18n'; -import { vectorStyles } from '../vector_style_defaults'; +import { VECTOR_STYLES } from '../vector_style_defaults'; export function getVectorStyleLabel(styleName) { switch (styleName) { - case vectorStyles.FILL_COLOR: + case VECTOR_STYLES.FILL_COLOR: return i18n.translate('xpack.maps.styles.vector.fillColorLabel', { defaultMessage: 'Fill color' }); - case vectorStyles.LINE_COLOR: + case VECTOR_STYLES.LINE_COLOR: return i18n.translate('xpack.maps.styles.vector.borderColorLabel', { defaultMessage: 'Border color' }); - case vectorStyles.LINE_WIDTH: + case VECTOR_STYLES.LINE_WIDTH: return i18n.translate('xpack.maps.styles.vector.borderWidthLabel', { defaultMessage: 'Border width' }); - case vectorStyles.ICON_SIZE: + case VECTOR_STYLES.ICON_SIZE: return i18n.translate('xpack.maps.styles.vector.symbolSizeLabel', { defaultMessage: 'Symbol size' }); - case vectorStyles.ICON_ORIENTATION: + case VECTOR_STYLES.ICON_ORIENTATION: return i18n.translate('xpack.maps.styles.vector.orientationLabel', { defaultMessage: 'Symbol orientation' }); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js index 35c7066b7fd0f..dc5098c4d6d4d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js @@ -81,18 +81,24 @@ export class StylePropertyLegendRow extends Component { } render() { - const { range, style } = this.props; if (this._excludeFromHeader()) { return null; } const header = style.renderHeader(); + + const min = this._formatValue(_.get(range, 'min', EMPTY_VALUE)); + const minLabel = this.props.style.isFieldMetaEnabled() && range && range.isMinOutsideStdRange ? `< ${min}` : min; + + const max = this._formatValue(_.get(range, 'max', EMPTY_VALUE)); + const maxLabel = this.props.style.isFieldMetaEnabled() && range && range.isMaxOutsideStdRange ? `> ${max}` : max; + return ( diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js index d1de8e0fe6b4a..9686214fec9fe 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component, Fragment } from 'react'; import { VectorStyle } from '../vector_style'; import { i18n } from '@kbn/i18n'; +import { FieldMetaOptionsPopover } from './field_meta_options_popover'; import { getVectorStyleLabel } from './get_vector_style_label'; import { EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiFormRow, EuiButtonToggle } from '@elastic/eui'; -export class StaticDynamicStyleRow extends React.Component { +export class StaticDynamicStyleRow extends Component { // Store previous options locally so when type is toggled, // previous style options can be used. prevStaticStyleOptions = this.props.defaultStaticStyleOptions; @@ -29,6 +30,17 @@ export class StaticDynamicStyleRow extends React.Component { return this.props.styleProperty.getOptions(); } + _onFieldMetaOptionsChange = fieldMetaOptions => { + const styleDescriptor = { + type: VectorStyle.STYLE_TYPE.DYNAMIC, + options: { + ...this._getStyleOptions(), + fieldMetaOptions + } + }; + this.props.handlePropertyChange(this.props.styleProperty.getStyleName(), styleDescriptor); + } + _onStaticStyleChange = options => { const styleDescriptor = { type: VectorStyle.STYLE_TYPE.STATIC, @@ -64,11 +76,17 @@ export class StaticDynamicStyleRow extends React.Component { if (this._isDynamic()) { const DynamicSelector = this.props.DynamicSelector; return ( - + + + + ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 3043d57c04037..d848b9274d071 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -22,7 +22,7 @@ import { SYMBOLIZE_AS_ICON } from '../vector_constants'; import { i18n } from '@kbn/i18n'; import { SYMBOL_OPTIONS } from '../symbol_utils'; -import { EuiSpacer, EuiButtonGroup } from '@elastic/eui'; +import { EuiSpacer, EuiButtonGroup, EuiFormRow, EuiSwitch } from '@elastic/eui'; export class VectorStyleEditor extends Component { state = { @@ -117,6 +117,14 @@ export class VectorStyleEditor extends Component { return [...this.state.dateFields, ...this.state.numberFields]; } + _handleSelectedFeatureChange = selectedFeature => { + this.setState({ selectedFeature }); + }; + + _onIsTimeAwareChange = event => { + this.props.onIsTimeAwareChange(event.target.checked); + }; + _renderFillColor() { return ( { - this.setState({ selectedFeature }); - }; - - render() { + _renderProperties() { const { supportedFeatures, selectedFeature } = this.state; if (!supportedFeatures) { @@ -302,4 +306,34 @@ export class VectorStyleEditor extends Component { ); } + + _renderIsTimeAwareSwitch() { + if (!this.props.showIsTimeAware) { + return null; + } + + return ( + + + + ); + } + + render() { + return ( + + {this._renderProperties()} + {this._renderIsTimeAwareSwitch()} + + ); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js index 4b4b853c274cb..d56db31d17067 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js @@ -50,7 +50,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { } isCustomColorRamp() { - return !!this._options.customColorRamp; + return this._options.useCustomColorRamp; } supportsFeatureState() { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js index fb4ffd8cce4b4..afbe924e1afb8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js @@ -7,14 +7,14 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { getComputedFieldName } from '../style_util'; -import { vectorStyles } from '../vector_style_defaults'; +import { VECTOR_STYLES } from '../vector_style_defaults'; export class DynamicOrientationProperty extends DynamicStyleProperty { syncIconRotationWithMb(symbolLayerId, mbMap) { if (this._options.field && this._options.field.name) { - const targetName = getComputedFieldName(vectorStyles.ICON_ORIENTATION, this._options.field.name); + const targetName = getComputedFieldName(VECTOR_STYLES.ICON_ORIENTATION, this._options.field.name); // Using property state instead of feature-state because layout properties do not support feature-state mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', ['coalesce', ['get', targetName], 0]); } else { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js index bd011b27d81c8..b4e6cf7be1701 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js @@ -8,7 +8,7 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { getComputedFieldName } from '../style_util'; import { HALF_LARGE_MAKI_ICON_SIZE, LARGE_MAKI_ICON_SIZE, SMALL_MAKI_ICON_SIZE } from '../symbol_utils'; -import { vectorStyles } from '../vector_style_defaults'; +import { VECTOR_STYLES } from '../vector_style_defaults'; import _ from 'lodash'; import { CircleIcon } from '../components/legend/circle_icon'; import React, { Fragment } from 'react'; @@ -55,7 +55,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { mbMap.setLayoutProperty(symbolLayerId, 'icon-image', `${symbolId}-${iconPixels}`); const halfIconPixels = iconPixels / 2; - const targetName = getComputedFieldName(vectorStyles.ICON_SIZE, this._options.field.name); + const targetName = getComputedFieldName(VECTOR_STYLES.ICON_SIZE, this._options.field.name); // Using property state instead of feature-state because layout properties do not support feature-state mbMap.setLayoutProperty(symbolLayerId, 'icon-size', [ 'interpolate', @@ -112,9 +112,9 @@ export class DynamicSizeProperty extends DynamicStyleProperty { renderHeader() { let icons; - if (this.getStyleName() === vectorStyles.LINE_WIDTH) { + if (this.getStyleName() === VECTOR_STYLES.LINE_WIDTH) { icons = getLineWidthIcons(); - } else if (this.getStyleName() === vectorStyles.ICON_SIZE) { + } else if (this.getStyleName() === VECTOR_STYLES.ICON_SIZE) { icons = getSymbolSizeIcons(); } else { return null; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index e87bcc12c99be..a72502f9f17fb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ - +import _ from 'lodash'; import { AbstractStyleProperty } from './style_property'; +import { DEFAULT_SIGMA } from '../vector_style_defaults'; import { STYLE_TYPE } from '../../../../../common/constants'; export class DynamicStyleProperty extends AbstractStyleProperty { @@ -32,6 +33,22 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return this._field.getOrigin(); } + isFieldMetaEnabled() { + const fieldMetaOptions = this.getFieldMetaOptions(); + return this.supportsFieldMeta() && _.get(fieldMetaOptions, 'isEnabled', true); + } + + supportsFieldMeta() { + return this.isComplete() && this.isScaled() && this._field.supportsFieldMeta(); + } + + async getFieldMetaRequest() { + const fieldMetaOptions = this.getFieldMetaOptions(); + return this._field.getFieldMetaRequest({ + sigma: _.get(fieldMetaOptions, 'sigma', DEFAULT_SIGMA), + }); + } + supportsFeatureState() { return true; } @@ -39,4 +56,8 @@ export class DynamicStyleProperty extends AbstractStyleProperty { isScaled() { return true; } + + getFieldMetaOptions() { + return _.get(this.getOptions(), 'fieldMetaOptions', {}); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js index 69caaca080138..699955fe6542a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ - export function getComputedFieldName(styleName, fieldName) { return `${getComputedFieldNamePrefix(fieldName)}__${styleName}`; } @@ -12,3 +11,19 @@ export function getComputedFieldName(styleName, fieldName) { export function getComputedFieldNamePrefix(fieldName) { return `__kbn__dynamic__${fieldName}`; } + +export function scaleValue(value, range) { + if (isNaN(value) || !range) { + return -1; //Nothing to scale, put outside scaled range + } + + if (range.delta === 0 || value >= range.max) { + return 1; //snap to end of scaled range + } + + if (value <= range.min) { + return 0; //snap to beginning of scaled range + } + + return (value - range.min) / range.delta; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js new file mode 100644 index 0000000000000..a25e3bf8684c9 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js @@ -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 { scaleValue } from './style_util'; + +describe('scaleValue', () => { + test('Should scale value between 0 and 1', () => { + expect(scaleValue(5, { min: 0, max: 10, delta: 10 })).toBe(0.5); + }); + + test('Should snap value less then range min to 0', () => { + expect(scaleValue(-1, { min: 0, max: 10, delta: 10 })).toBe(0); + }); + + test('Should snap value greater then range max to 1', () => { + expect(scaleValue(11, { min: 0, max: 10, delta: 10 })).toBe(1); + }); + + test('Should snap value to 1 when tere is not range delta', () => { + expect(scaleValue(10, { min: 10, max: 10, delta: 0 })).toBe(1); + }); + + test('Should put value as -1 when value is not provided', () => { + expect(scaleValue(undefined, { min: 0, max: 10, delta: 10 })).toBe(-1); + }); + + test('Should put value as -1 when range is not provided', () => { + expect(scaleValue(5, undefined)).toBe(-1); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 45a1636e5c033..53794f2043aad 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -7,15 +7,21 @@ import _ from 'lodash'; import React from 'react'; import { VectorStyleEditor } from './components/vector_style_editor'; -import { getDefaultProperties, vectorStyles } from './vector_style_defaults'; +import { getDefaultProperties, VECTOR_STYLES } from './vector_style_defaults'; import { AbstractStyle } from '../abstract_style'; -import { GEO_JSON_TYPE, FIELD_ORIGIN, STYLE_TYPE } from '../../../../common/constants'; +import { + GEO_JSON_TYPE, + FIELD_ORIGIN, + STYLE_TYPE, + SOURCE_META_ID_ORIGIN, + LAYER_STYLE_TYPE, +} from '../../../../common/constants'; import { VectorIcon } from './components/legend/vector_icon'; import { VectorStyleLegend } from './components/legend/vector_style_legend'; import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; import { SYMBOLIZE_AS_CIRCLE, SYMBOLIZE_AS_ICON } from './vector_constants'; import { getMakiSymbolAnchor } from './symbol_utils'; -import { getComputedFieldName } from './style_util'; +import { getComputedFieldName, scaleValue } from './style_util'; import { StaticStyleProperty } from './properties/static_style_property'; import { DynamicStyleProperty } from './properties/dynamic_style_property'; import { DynamicSizeProperty } from './properties/dynamic_size_property'; @@ -31,12 +37,13 @@ const POLYGONS = [GEO_JSON_TYPE.POLYGON, GEO_JSON_TYPE.MULTI_POLYGON]; export class VectorStyle extends AbstractStyle { - static type = 'VECTOR'; + static type = LAYER_STYLE_TYPE.VECTOR; static STYLE_TYPE = STYLE_TYPE; - static createDescriptor(properties = {}) { + static createDescriptor(properties = {}, isTimeAware = true) { return { type: VectorStyle.type, - properties: { ...getDefaultProperties(), ...properties } + properties: { ...getDefaultProperties(), ...properties }, + isTimeAware, }; } @@ -50,15 +57,15 @@ export class VectorStyle extends AbstractStyle { this._layer = layer; this._descriptor = { ...descriptor, - ...VectorStyle.createDescriptor(descriptor.properties), + ...VectorStyle.createDescriptor(descriptor.properties, descriptor.isTimeAware), }; - this._lineColorStyleProperty = this._makeColorProperty(this._descriptor.properties[vectorStyles.LINE_COLOR], vectorStyles.LINE_COLOR); - this._fillColorStyleProperty = this._makeColorProperty(this._descriptor.properties[vectorStyles.FILL_COLOR], vectorStyles.FILL_COLOR); - this._lineWidthStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.LINE_WIDTH], vectorStyles.LINE_WIDTH); - this._iconSizeStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.ICON_SIZE], vectorStyles.ICON_SIZE); + this._lineColorStyleProperty = this._makeColorProperty(this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], VECTOR_STYLES.LINE_COLOR); + this._fillColorStyleProperty = this._makeColorProperty(this._descriptor.properties[VECTOR_STYLES.FILL_COLOR], VECTOR_STYLES.FILL_COLOR); + this._lineWidthStyleProperty = this._makeSizeProperty(this._descriptor.properties[VECTOR_STYLES.LINE_WIDTH], VECTOR_STYLES.LINE_WIDTH); + this._iconSizeStyleProperty = this._makeSizeProperty(this._descriptor.properties[VECTOR_STYLES.ICON_SIZE], VECTOR_STYLES.ICON_SIZE); // eslint-disable-next-line max-len - this._iconOrientationProperty = this._makeOrientationProperty(this._descriptor.properties[vectorStyles.ICON_ORIENTATION], vectorStyles.ICON_ORIENTATION); + this._iconOrientationProperty = this._makeOrientationProperty(this._descriptor.properties[VECTOR_STYLES.ICON_ORIENTATION], VECTOR_STYLES.ICON_ORIENTATION); } _getAllStyleProperties() { @@ -72,13 +79,22 @@ export class VectorStyle extends AbstractStyle { } renderEditor({ layer, onStyleDescriptorChange }) { - const styleProperties = { ...this.getRawProperties() }; + const rawProperties = this.getRawProperties(); const handlePropertyChange = (propertyName, settings) => { - styleProperties[propertyName] = settings;//override single property, but preserve the rest - const vectorStyleDescriptor = VectorStyle.createDescriptor(styleProperties); + rawProperties[propertyName] = settings;//override single property, but preserve the rest + const vectorStyleDescriptor = VectorStyle.createDescriptor(rawProperties, this.isTimeAware()); onStyleDescriptorChange(vectorStyleDescriptor); }; + const onIsTimeAwareChange = isTimeAware => { + const vectorStyleDescriptor = VectorStyle.createDescriptor(rawProperties, isTimeAware); + onStyleDescriptorChange(vectorStyleDescriptor); + }; + + const propertiesWithFieldMeta = this.getDynamicPropertiesArray().filter(dynamicStyleProp => { + return dynamicStyleProp.isFieldMetaEnabled(); + }); + return ( 0} /> ); } @@ -156,7 +175,7 @@ export class VectorStyle extends AbstractStyle { nextStyleDescriptor: VectorStyle.createDescriptor({ ...originalProperties, ...updatedProperties, - }) + }, this.isTimeAware()) }; } @@ -239,6 +258,10 @@ export class VectorStyle extends AbstractStyle { return fieldNames; } + isTimeAware() { + return this._descriptor.isTimeAware; + } + getRawProperties() { return this._descriptor.properties || {}; } @@ -277,7 +300,56 @@ export class VectorStyle extends AbstractStyle { } _getFieldRange = (fieldName) => { - return _.get(this._descriptor, ['__styleMeta', fieldName]); + const fieldRangeFromLocalFeatures = _.get(this._descriptor, ['__styleMeta', fieldName]); + const dynamicProps = this.getDynamicPropertiesArray(); + const dynamicProp = dynamicProps.find(dynamicProp => { return fieldName === dynamicProp.getField().getName(); }); + + if (!dynamicProp || !dynamicProp.isFieldMetaEnabled()) { + return fieldRangeFromLocalFeatures; + } + + let dataRequestId; + if (dynamicProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { + dataRequestId = SOURCE_META_ID_ORIGIN; + } else { + const join = this._layer.getValidJoins().find(join => { + const matchingField = join.getRightJoinSource().getMetricFieldForName(fieldName); + return !!matchingField; + }); + if (join) { + dataRequestId = join.getSourceMetaDataRequestId(); + } + } + + if (!dataRequestId) { + return fieldRangeFromLocalFeatures; + } + + const styleMetaDataRequest = this._layer._findDataRequestForSource(dataRequestId); + if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { + return fieldRangeFromLocalFeatures; + } + + const data = styleMetaDataRequest.getData(); + const field = dynamicProp.getField(); + const realFieldName = field.getESDocFieldName ? field.getESDocFieldName() : field.getName(); + const stats = data[realFieldName]; + if (!stats) { + return fieldRangeFromLocalFeatures; + } + + const sigma = _.get(dynamicProp.getFieldMetaOptions(), 'sigma', 3); + const stdLowerBounds = stats.avg - (stats.std_deviation * sigma); + const stdUpperBounds = stats.avg + (stats.std_deviation * sigma); + const min = Math.max(stats.min, stdLowerBounds); + const max = Math.min(stats.max, stdUpperBounds); + return { + min, + max, + delta: max - min, + isMinOutsideStdRange: stats.min < stdLowerBounds, + isMaxOutsideStdRange: stats.max > stdUpperBounds, + }; } getIcon = () => { @@ -289,8 +361,8 @@ export class VectorStyle extends AbstractStyle { ); @@ -321,7 +393,7 @@ export class VectorStyle extends AbstractStyle { // To work around this limitation, some styling values must fall back to geojson property values. let supportsFeatureState; let isScaled; - if (styleProperty.getStyleName() === vectorStyles.ICON_SIZE + if (styleProperty.getStyleName() === VECTOR_STYLES.ICON_SIZE && this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_ICON) { supportsFeatureState = false; isScaled = true; @@ -380,13 +452,7 @@ export class VectorStyle extends AbstractStyle { const value = parseFloat(feature.properties[name]); let styleValue; if (isScaled) { - if (isNaN(value) || !range) {//cannot scale - styleValue = -1;//put outside range - } else if (range.delta === 0) {//values are identical - styleValue = 1;//snap to end of color range - } else { - styleValue = (value - range.min) / range.delta; - } + styleValue = scaleValue(value, range); } else { if (isNaN(value)) { styleValue = 0; @@ -450,7 +516,6 @@ export class VectorStyle extends AbstractStyle { } _makeField(fieldDescriptor) { - if (!fieldDescriptor || !fieldDescriptor.name) { return null; } @@ -473,8 +538,6 @@ export class VectorStyle extends AbstractStyle { } else { throw new Error(`Unknown origin-type ${fieldDescriptor.origin}`); } - - } _makeSizeProperty(descriptor, styleName) { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js index ea4228430d13d..b834fb842389e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js @@ -16,8 +16,9 @@ const DEFAULT_ICON = 'airfield'; export const DEFAULT_MIN_SIZE = 1; export const DEFAULT_MAX_SIZE = 64; +export const DEFAULT_SIGMA = 3; -export const vectorStyles = { +export const VECTOR_STYLES = { SYMBOL: 'symbol', FILL_COLOR: 'fillColor', LINE_COLOR: 'lineColor', @@ -29,7 +30,7 @@ export const vectorStyles = { export function getDefaultProperties(mapColors = []) { return { ...getDefaultStaticProperties(mapColors), - [vectorStyles.SYMBOL]: { + [VECTOR_STYLES.SYMBOL]: { options: { symbolizeAs: SYMBOLIZE_AS_CIRCLE, symbolId: DEFAULT_ICON, @@ -48,31 +49,31 @@ export function getDefaultStaticProperties(mapColors = []) { return { - [vectorStyles.FILL_COLOR]: { + [VECTOR_STYLES.FILL_COLOR]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { color: nextFillColor, } }, - [vectorStyles.LINE_COLOR]: { + [VECTOR_STYLES.LINE_COLOR]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { color: nextLineColor } }, - [vectorStyles.LINE_WIDTH]: { + [VECTOR_STYLES.LINE_WIDTH]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { size: 1 } }, - [vectorStyles.ICON_SIZE]: { + [VECTOR_STYLES.ICON_SIZE]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { size: DEFAULT_ICON_SIZE } }, - [vectorStyles.ICON_ORIENTATION]: { + [VECTOR_STYLES.ICON_ORIENTATION]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { orientation: 0 @@ -83,40 +84,60 @@ export function getDefaultStaticProperties(mapColors = []) { export function getDefaultDynamicProperties() { return { - [vectorStyles.FILL_COLOR]: { + [VECTOR_STYLES.FILL_COLOR]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.LINE_COLOR]: { + [VECTOR_STYLES.LINE_COLOR]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.LINE_WIDTH]: { + [VECTOR_STYLES.LINE_WIDTH]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { minSize: DEFAULT_MIN_SIZE, maxSize: DEFAULT_MAX_SIZE, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.ICON_SIZE]: { + [VECTOR_STYLES.ICON_SIZE]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { minSize: DEFAULT_MIN_SIZE, maxSize: DEFAULT_MAX_SIZE, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.ICON_ORIENTATION]: { + [VECTOR_STYLES.ICON_ORIENTATION]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js index 610c704b34ec6..557a2bf869987 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js @@ -128,3 +128,22 @@ export async function canSkipSourceUpdate({ source, prevDataRequest, nextMeta }) && !updateDueToPrecisionChange && !updateDueToSourceMetaChange; } + +export function canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }) { + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta(); + if (!prevMeta) { + return false; + } + + const updateDueToFields = !_.isEqual(prevMeta.dynamicStyleFields, nextMeta.dynamicStyleFields); + + const updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); + + const updateDueToIsTimeAware = nextMeta.isTimeAware !== prevMeta.isTimeAware; + const updateDueToTime = nextMeta.isTimeAware ? !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters) : false; + + return !updateDueToFields && !updateDueToSourceQuery && !updateDueToIsTimeAware && !updateDueToTime; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js index 77359a6def48f..24728f2ac95fd 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js @@ -126,7 +126,8 @@ describe('canSkipSourceUpdate', () => { applyGlobalQuery: prevApplyGlobalQuery, filters: prevFilters, query: prevQuery, - } + }, + data: {} }); it('can skip update when filter changes', async () => { @@ -210,7 +211,8 @@ describe('canSkipSourceUpdate', () => { applyGlobalQuery: prevApplyGlobalQuery, filters: prevFilters, query: prevQuery, - } + }, + data: {} }); it('can not skip update when filter changes', async () => { diff --git a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js b/x-pack/legacy/plugins/maps/public/layers/util/data_request.js index 95b82aa292884..12d57afbe1c87 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/data_request.js @@ -22,7 +22,7 @@ export class DataRequest { } getMeta() { - return _.get(this._descriptor, 'dataMeta', {}); + return this.hasData() ? _.get(this._descriptor, 'dataMeta', {}) : _.get(this._descriptor, 'dataMetaAtStart', {}); } hasData() { diff --git a/x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js b/x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js new file mode 100644 index 0000000000000..54d8794b1e3cf --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js @@ -0,0 +1,11 @@ +/* + * 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 { METRIC_TYPE } from '../../../common/constants'; + +export function isMetricCountable(aggType) { + return [METRIC_TYPE.COUNT, METRIC_TYPE.SUM, METRIC_TYPE.UNIQUE_COUNT].includes(aggType); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 57126bb7681b8..7e831115e6dba 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -12,16 +12,19 @@ import { InnerJoin } from './joins/inner_join'; import { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN, + SOURCE_META_ID_ORIGIN, FEATURE_VISIBLE_PROPERTY_NAME, EMPTY_FEATURE_COLLECTION, - LAYER_TYPE + LAYER_TYPE, + FIELD_ORIGIN, + LAYER_STYLE_TYPE, } from '../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; -import { canSkipSourceUpdate } from './util/can_skip_fetch'; +import { canSkipSourceUpdate, canSkipStyleMetaUpdate } from './util/can_skip_fetch'; import { assignFeatureIds } from './util/assign_feature_ids'; import { getFillFilterExpression, @@ -88,7 +91,7 @@ export class VectorLayer extends AbstractLayer { const joins = this.getValidJoins(); for (let i = 0; i < joins.length; i++) { - const joinDataRequest = this.getDataRequest(joins[i].getSourceId()); + const joinDataRequest = this.getDataRequest(joins[i].getSourceDataRequestId()); if (!joinDataRequest || !joinDataRequest.hasData()) { return false; } @@ -229,12 +232,10 @@ export class VectorLayer extends AbstractLayer { return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); } - - async _syncJoin({ join, startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { const joinSource = join.getRightJoinSource(); - const sourceDataId = join.getSourceId(); + const sourceDataId = join.getSourceDataRequestId(); const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); const searchFilters = { ...dataFilters, @@ -287,6 +288,7 @@ export class VectorLayer extends AbstractLayer { async _syncJoins(syncContext) { const joinSyncs = this.getValidJoins().map(async join => { + await this._syncJoinStyleMeta(syncContext, join); return this._syncJoin({ join, ...syncContext }); }); @@ -350,7 +352,7 @@ export class VectorLayer extends AbstractLayer { startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { - const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); + const requestToken = Symbol(`layer-source-data:${this.getId()}`); const searchFilters = this._getSearchFilters(dataFilters); const prevDataRequest = this.getSourceDataRequest(); @@ -389,11 +391,89 @@ export class VectorLayer extends AbstractLayer { } } + async _syncSourceStyleMeta(syncContext) { + if (this._style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { + return; + } + + return this._syncStyleMeta({ + source: this._source, + sourceQuery: this.getQuery(), + dataRequestId: SOURCE_META_ID_ORIGIN, + dynamicStyleProps: this._style.getDynamicPropertiesArray().filter(dynamicStyleProp => { + return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE && dynamicStyleProp.isFieldMetaEnabled(); + }), + ...syncContext + }); + } + + async _syncJoinStyleMeta(syncContext, join) { + const joinSource = join.getRightJoinSource(); + return this._syncStyleMeta({ + source: joinSource, + sourceQuery: joinSource.getWhereQuery(), + dataRequestId: join.getSourceMetaDataRequestId(), + dynamicStyleProps: this._style.getDynamicPropertiesArray().filter(dynamicStyleProp => { + const matchingField = joinSource.getMetricFieldForName(dynamicStyleProp.getField().getName()); + return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN + && !!matchingField + && dynamicStyleProp.isFieldMetaEnabled(); + }), + ...syncContext + }); + } + + async _syncStyleMeta({ + source, + sourceQuery, + dataRequestId, + dynamicStyleProps, + dataFilters, + startLoading, + stopLoading, + onLoadError, + registerCancelCallback + }) { + + if (!source.isESSource() || dynamicStyleProps.length === 0) { + return; + } + + const dynamicStyleFields = dynamicStyleProps.map(dynamicStyleProp => { + return dynamicStyleProp.getField().getName(); + }); + + const nextMeta = { + dynamicStyleFields: _.uniq(dynamicStyleFields).sort(), + sourceQuery, + isTimeAware: this._style.isTimeAware() && await source.isTimeAware(), + timeFilters: dataFilters.timeFilters, + }; + const prevDataRequest = this._findDataRequestForSource(dataRequestId); + const canSkipFetch = canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }); + if (canSkipFetch) { + return; + } + + const requestToken = Symbol(`layer-${this.getId()}-style-meta`); + try { + startLoading(dataRequestId, requestToken, nextMeta); + const layerName = await this.getDisplayName(); + const styleMeta = await source.loadStylePropsMeta(layerName, this._style, dynamicStyleProps, registerCancelCallback, nextMeta); + stopLoading(dataRequestId, requestToken, styleMeta, nextMeta); + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + onLoadError(dataRequestId, requestToken, error.message); + } + } + } + async syncData(syncContext) { if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; } + await this._syncSourceStyleMeta(syncContext); const sourceResult = await this._syncSource(syncContext); if ( !sourceResult.featureCollection || diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts index d7da585966758..ede0d3f394789 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts @@ -147,6 +147,10 @@ export const mockLineLayer = { }, minSize: 1, maxSize: 8, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, }, }, iconSize: { type: 'STATIC', options: { size: 10 } }, diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts index fd17e6eaeac64..637251eb64f70 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts @@ -210,6 +210,10 @@ export const getLineLayer = (indexPatternTitle: string, indexPatternId: string) }, minSize: 1, maxSize: 8, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, }, }, iconSize: { type: 'STATIC', options: { size: 10 } }, diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 1291e3dd10cff..a9d2601442aaa 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -411,7 +411,7 @@ "type": "envelope" }, "description": "", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"label\":\"EMS base layer (road_map)\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[\"name\"],\"applyGlobalQuery\":false,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.shape_name\",\"name\":\"__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name\",\"origin\":\"join\"},\"color\":\"Blues\"}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_join_0_index_pattern\"}}]}]", + "layerListJSON" : "[{\"id\":\"0hmz5\",\"label\":\"EMS base layer (road_map)\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[\"name\"],\"applyGlobalQuery\":false,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3},\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.shape_name\",\"name\":\"__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name\",\"origin\":\"join\"},\"color\":\"Blues\"}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_join_0_index_pattern\"}}]}]", "mapStateJSON": "{\"zoom\":3.02,\"center\":{\"lon\":77.33426,\"lat\":-0.04647},\"timeFilters\":{\"from\":\"now-17m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}", "title": "join example", "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[\"n1t6f\"]}" From 2ef6d8d8f7b4ed4b1a2a9a06c5305af1f9c88ae7 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 6 Dec 2019 17:46:25 +0100 Subject: [PATCH 12/35] Add pre-response http interceptor (#52366) * add onPreResponse interceptor * expose registerPreResponse to plugins * address comments * regen docs --- .../kibana-plugin-server.httpservicesetup.md | 1 + ....httpservicesetup.registeronpreresponse.md | 18 ++ .../core/server/kibana-plugin-server.md | 4 + ...-server.onpreresponseextensions.headers.md | 13 ++ ...a-plugin-server.onpreresponseextensions.md | 20 +++ ...bana-plugin-server.onpreresponsehandler.md | 13 ++ .../kibana-plugin-server.onpreresponseinfo.md | 20 +++ ...gin-server.onpreresponseinfo.statuscode.md | 11 ++ ...bana-plugin-server.onpreresponsetoolkit.md | 20 +++ ...plugin-server.onpreresponsetoolkit.next.md | 13 ++ src/core/server/http/http_server.ts | 50 ++---- src/core/server/http/http_service.mock.ts | 1 + src/core/server/http/index.ts | 6 + .../http/integration_tests/lifecycle.test.ts | 168 +++++++++++++++++- .../server/http/lifecycle/on_pre_response.ts | 155 ++++++++++++++++ src/core/server/http/types.ts | 13 ++ src/core/server/index.ts | 4 + src/core/server/legacy/legacy_service.ts | 1 + src/core/server/mocks.ts | 1 + src/core/server/plugins/plugin_context.ts | 1 + src/core/server/server.api.md | 22 +++ 21 files changed, 517 insertions(+), 38 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md create mode 100644 src/core/server/http/lifecycle/on_pre_response.ts diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index dba0ad8c8560c..25eebf1c06d01 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -23,6 +23,7 @@ export interface HttpServiceSetup | [registerAuth](./kibana-plugin-server.httpservicesetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. | | [registerOnPostAuth](./kibana-plugin-server.httpservicesetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. | | [registerOnPreAuth](./kibana-plugin-server.httpservicesetup.registeronpreauth.md) | (handler: OnPreAuthHandler) => void | To define custom logic to perform for incoming requests. | +| [registerOnPreResponse](./kibana-plugin-server.httpservicesetup.registeronpreresponse.md) | (handler: OnPreResponseHandler) => void | To define custom logic to perform for the server response. | | [registerRouteHandlerContext](./kibana-plugin-server.httpservicesetup.registerroutehandlercontext.md) | <T extends keyof RequestHandlerContext>(contextName: T, provider: RequestHandlerContextProvider<T>) => RequestHandlerContextContainer | Register a context provider for a route handler. | ## Example diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md new file mode 100644 index 0000000000000..9f0eaae8830e1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) > [registerOnPreResponse](./kibana-plugin-server.httpservicesetup.registeronpreresponse.md) + +## HttpServiceSetup.registerOnPreResponse property + +To define custom logic to perform for the server response. + +Signature: + +```typescript +registerOnPreResponse: (handler: OnPreResponseHandler) => void; +``` + +## Remarks + +Doesn't provide the whole response object. Supports extending response with custom headers. See [OnPreResponseHandler](./kibana-plugin-server.onpreresponsehandler.md). + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 9144742c9bb73..fceabd1237665 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -77,6 +77,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | | [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | +| [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) | Additional data to extend a response. | +| [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) | Response status code. | +| [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration schema and capabilities. | @@ -173,6 +176,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [MutatingOperationRefreshSetting](./kibana-plugin-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation | | [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | See [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md). | | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). | +| [OnPreResponseHandler](./kibana-plugin-server.onpreresponsehandler.md) | See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). | | [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | Dedicated type for plugin configuration schema. | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md new file mode 100644 index 0000000000000..8736020daf063 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) > [headers](./kibana-plugin-server.onpreresponseextensions.headers.md) + +## OnPreResponseExtensions.headers property + +additional headers to attach to the response + +Signature: + +```typescript +headers?: ResponseHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md new file mode 100644 index 0000000000000..e5aa624c39909 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) + +## OnPreResponseExtensions interface + +Additional data to extend a response. + +Signature: + +```typescript +export interface OnPreResponseExtensions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-server.onpreresponseextensions.headers.md) | ResponseHeaders | additional headers to attach to the response | + diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md b/docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md new file mode 100644 index 0000000000000..082de0a9b4aeb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseHandler](./kibana-plugin-server.onpreresponsehandler.md) + +## OnPreResponseHandler type + +See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). + +Signature: + +```typescript +export declare type OnPreResponseHandler = (request: KibanaRequest, preResponse: OnPreResponseInfo, toolkit: OnPreResponseToolkit) => OnPreResponseResult | Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md new file mode 100644 index 0000000000000..736b4298037cf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) + +## OnPreResponseInfo interface + +Response status code. + +Signature: + +```typescript +export interface OnPreResponseInfo +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [statusCode](./kibana-plugin-server.onpreresponseinfo.statuscode.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md new file mode 100644 index 0000000000000..4fd4529dc400f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) > [statusCode](./kibana-plugin-server.onpreresponseinfo.statuscode.md) + +## OnPreResponseInfo.statusCode property + +Signature: + +```typescript +statusCode: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md new file mode 100644 index 0000000000000..5525f5bf60284 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) + +## OnPreResponseToolkit interface + +A tool set defining an outcome of OnPreAuth interceptor for incoming request. + +Signature: + +```typescript +export interface OnPreResponseToolkit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [next](./kibana-plugin-server.onpreresponsetoolkit.next.md) | (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult | To pass request to the next handler | + diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md new file mode 100644 index 0000000000000..bfb5827b16b2f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) > [next](./kibana-plugin-server.onpreresponsetoolkit.next.md) + +## OnPreResponseToolkit.next property + +To pass request to the next handler + +Signature: + +```typescript +next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; +``` diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index f77184fb79ab6..244b3cca60f31 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import { Request, Server } from 'hapi'; +import { Server } from 'hapi'; import url from 'url'; import { Logger, LoggerFactory } from '../logging'; @@ -26,8 +25,9 @@ import { createServer, getListenerOptions, getServerOptions } from './http_tools import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; +import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response'; -import { ResponseHeaders, IRouter } from './router'; +import { IRouter } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -50,6 +50,7 @@ export interface HttpServerSetup { registerAuth: HttpServiceSetup['registerAuth']; registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; + registerOnPreResponse: HttpServiceSetup['registerOnPreResponse']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; auth: { get: GetAuthState; @@ -103,6 +104,7 @@ export class HttpServer { registerRouter: this.registerRouter.bind(this), registerOnPreAuth: this.registerOnPreAuth.bind(this), registerOnPostAuth: this.registerOnPostAuth.bind(this), + registerOnPreResponse: this.registerOnPreResponse.bind(this), createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => this.createCookieSessionStorageFactory(cookieOptions, config.basePath), registerAuth: this.registerAuth.bind(this), @@ -232,6 +234,14 @@ export class HttpServer { this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn, this.log)); } + private registerOnPreResponse(fn: OnPreResponseHandler) { + if (this.server === undefined) { + throw new Error('Server is not created yet'); + } + + this.server.ext('onPreResponse', adoptToHapiOnPreResponseFormat(fn, this.log)); + } + private async createCookieSessionStorageFactory( cookieOptions: SessionStorageCookieOptions, basePath?: string @@ -289,39 +299,9 @@ export class HttpServer { // https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions this.server.auth.default('session'); - this.server.ext('onPreResponse', (request, t) => { + this.registerOnPreResponse((request, preResponseInfo, t) => { const authResponseHeaders = this.authResponseHeaders.get(request); - this.extendResponseWithHeaders(request, authResponseHeaders); - return t.continue; - }); - } - - private extendResponseWithHeaders(request: Request, headers?: ResponseHeaders) { - const response = request.response; - if (!headers || !response) return; - - if (response instanceof Error) { - this.findHeadersIntersection(response.output.headers, headers); - // hapi wraps all error response in Boom object internally - response.output.headers = { - ...response.output.headers, - ...(headers as any), // hapi types don't specify string[] as valid value - }; - } else { - for (const [headerName, headerValue] of Object.entries(headers)) { - this.findHeadersIntersection(response.headers, headers); - response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value - } - } - } - - // NOTE: responseHeaders contains not a full list of response headers, but only explicitly set on a response object. - // any headers added by hapi internally, like `content-type`, `content-length`, etc. do not present here. - private findHeadersIntersection(responseHeaders: ResponseHeaders, headers: ResponseHeaders) { - Object.keys(headers).forEach(headerName => { - if (responseHeaders[headerName] !== undefined) { - this.log.warn(`Server rewrites a response header [${headerName}].`); - } + return t.next({ headers: authResponseHeaders }); }); } } diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index c7f6cdb2bb422..fb3716c42b831 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -51,6 +51,7 @@ const createSetupContractMock = () => { registerAuth: jest.fn(), registerOnPostAuth: jest.fn(), registerRouteHandlerContext: jest.fn(), + registerOnPreResponse: jest.fn(), createRouter: jest.fn().mockImplementation(() => mockRouter.create({})), basePath: createBasePathMock(), auth: { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index f9a3a91ec18ad..21de3945f1044 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -64,6 +64,12 @@ export { AuthResultType, } from './lifecycle/auth'; export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; +export { + OnPreResponseHandler, + OnPreResponseToolkit, + OnPreResponseExtensions, + OnPreResponseInfo, +} from './lifecycle/on_pre_response'; export { SessionStorageFactory, SessionStorage } from './session_storage'; export { SessionStorageCookieOptions, diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 2a32db77377a4..0edbcf19d3209 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -161,7 +161,7 @@ describe('OnPreAuth', () => { expect(result.header['www-authenticate']).toBe('challenge'); }); - it("doesn't expose error details if interceptor throws", async () => { + it('does not expose error details if interceptor throws', async () => { const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); @@ -734,7 +734,7 @@ describe('Auth', () => { expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` Array [ Array [ - "Server rewrites a response header [www-authenticate].", + "onPreResponseHandler rewrote a response header [www-authenticate].", ], ] `); @@ -769,7 +769,7 @@ describe('Auth', () => { expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` Array [ Array [ - "Server rewrites a response header [www-authenticate].", + "onPreResponseHandler rewrote a response header [www-authenticate].", ], ] `); @@ -893,3 +893,165 @@ describe('Auth', () => { .expect(200, { customField: 'undefined' }); }); }); + +describe('OnPreResponse', () => { + it('supports registering response inceptors', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' })); + + const callingOrder: string[] = []; + registerOnPreResponse((req, res, t) => { + callingOrder.push('first'); + return t.next(); + }); + + registerOnPreResponse((req, res, t) => { + callingOrder.push('second'); + return t.next(); + }); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, 'ok'); + + expect(callingOrder).toEqual(['first', 'second']); + }); + + it('supports additional headers attachments', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => + res.ok({ + headers: { + 'x-my-header': 'foo', + }, + }) + ); + + registerOnPreResponse((req, res, t) => + t.next({ + headers: { + 'x-kibana-header': 'value', + }, + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.header['x-kibana-header']).toBe('value'); + expect(result.header['x-my-header']).toBe('foo'); + }); + + it('logs a warning if interceptor rewrites response header', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => + res.ok({ + headers: { 'x-kibana-header': 'value' }, + }) + ); + + registerOnPreResponse((req, res, t) => + t.next({ + headers: { 'x-kibana-header': 'value' }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "onPreResponseHandler rewrote a response header [x-kibana-header].", + ], + ] + `); + }); + + it("doesn't expose error details if interceptor throws", async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(undefined)); + registerOnPreResponse((req, res, t) => { + throw new Error('reason'); + }); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: reason], + ], + ] + `); + }); + + it('returns internal error if interceptor returns unexpected result', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok()); + registerOnPreResponse((req, res, t) => ({} as any)); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unexpected result from OnPreResponse. Expected OnPreResponseResult, but given: [object Object].], + ], + ] + `); + }); + + it('cannot change response statusCode', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + registerOnPreResponse((req, res, t) => { + res.statusCode = 500; + return t.next(); + }); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' })); + + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200); + }); +}); diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts new file mode 100644 index 0000000000000..45d7478df9805 --- /dev/null +++ b/src/core/server/http/lifecycle/on_pre_response.ts @@ -0,0 +1,155 @@ +/* + * 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 { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi'; +import Boom from 'boom'; +import { Logger } from '../../logging'; + +import { HapiResponseAdapter, KibanaRequest, ResponseHeaders } from '../router'; + +enum ResultType { + next = 'next', +} + +interface Next { + type: ResultType.next; + headers?: ResponseHeaders; +} + +/** + * @internal + */ +type OnPreResponseResult = Next; + +/** + * Additional data to extend a response. + * @public + */ +export interface OnPreResponseExtensions { + /** additional headers to attach to the response */ + headers?: ResponseHeaders; +} + +/** + * Response status code. + * @public + */ +export interface OnPreResponseInfo { + statusCode: number; +} + +const preResponseResult = { + next(responseExtensions?: OnPreResponseExtensions): OnPreResponseResult { + return { type: ResultType.next, headers: responseExtensions?.headers }; + }, + isNext(result: OnPreResponseResult): result is Next { + return result && result.type === ResultType.next; + }, +}; + +/** + * A tool set defining an outcome of OnPreAuth interceptor for incoming request. + * @public + */ +export interface OnPreResponseToolkit { + /** To pass request to the next handler */ + next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; +} + +const toolkit: OnPreResponseToolkit = { + next: preResponseResult.next, +}; + +/** + * See {@link OnPreAuthToolkit}. + * @public + */ +export type OnPreResponseHandler = ( + request: KibanaRequest, + preResponse: OnPreResponseInfo, + toolkit: OnPreResponseToolkit +) => OnPreResponseResult | Promise; + +/** + * @public + * Adopt custom request interceptor to Hapi lifecycle system. + * @param fn - an extension point allowing to perform custom logic for + * incoming HTTP requests. + */ +export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Logger) { + return async function interceptPreResponse( + request: Request, + responseToolkit: HapiResponseToolkit + ): Promise { + const response = request.response; + + try { + if (response) { + const statusCode: number = isBoom(response) + ? response.output.statusCode + : response.statusCode; + + const result = await fn(KibanaRequest.from(request), { statusCode }, toolkit); + if (!preResponseResult.isNext(result)) { + throw new Error( + `Unexpected result from OnPreResponse. Expected OnPreResponseResult, but given: ${result}.` + ); + } + if (result.headers) { + if (isBoom(response)) { + findHeadersIntersection(response.output.headers, result.headers, log); + // hapi wraps all error response in Boom object internally + response.output.headers = { + ...response.output.headers, + ...(result.headers as any), // hapi types don't specify string[] as valid value + }; + } else { + for (const [headerName, headerValue] of Object.entries(result.headers)) { + findHeadersIntersection(response.headers, result.headers, log); + response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value + } + } + } + } + } catch (error) { + log.error(error); + const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); + return hapiResponseAdapter.toInternalError(); + } + return responseToolkit.continue; + }; +} + +function isBoom(response: any): response is Boom { + return response instanceof Boom; +} + +// NOTE: responseHeaders contains not a full list of response headers, but only explicitly set on a response object. +// any headers added by hapi internally, like `content-type`, `content-length`, etc. are not present here. +function findHeadersIntersection( + responseHeaders: ResponseHeaders, + headers: ResponseHeaders, + log: Logger +) { + Object.keys(headers).forEach(headerName => { + if (responseHeaders[headerName] !== undefined) { + log.warn(`onPreResponseHandler rewrote a response header [${headerName}].`); + } + }); +} diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 2c3dfedd1d181..94c1982a18c0a 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -24,6 +24,7 @@ import { SessionStorageFactory } from './session_storage'; import { AuthenticationHandler } from './lifecycle/auth'; import { OnPreAuthHandler } from './lifecycle/on_pre_auth'; import { OnPostAuthHandler } from './lifecycle/on_post_auth'; +import { OnPreResponseHandler } from './lifecycle/on_pre_response'; import { IBasePath } from './base_path_service'; import { PluginOpaqueId, RequestHandlerContext } from '..'; @@ -163,6 +164,18 @@ export interface HttpServiceSetup { */ registerOnPostAuth: (handler: OnPostAuthHandler) => void; + /** + * To define custom logic to perform for the server response. + * + * @remarks + * Doesn't provide the whole response object. + * Supports extending response with custom headers. + * See {@link OnPreResponseHandler}. + * + * @param handler {@link OnPreResponseHandler} - function to call. + */ + registerOnPreResponse: (handler: OnPreResponseHandler) => void; + /** * Access or manipulate the Kibana base path * See {@link IBasePath}. diff --git a/src/core/server/index.ts b/src/core/server/index.ts index efff85142c3e4..57156322e2849 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -105,6 +105,10 @@ export { OnPreAuthToolkit, OnPostAuthHandler, OnPostAuthToolkit, + OnPreResponseHandler, + OnPreResponseToolkit, + OnPreResponseExtensions, + OnPreResponseInfo, RedirectResponseOptions, RequestHandler, RequestHandlerContextContainer, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 5d111884144c1..fcf0c45c17db8 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -270,6 +270,7 @@ export class LegacyService implements CoreService { registerOnPreAuth: setupDeps.core.http.registerOnPreAuth, registerAuth: setupDeps.core.http.registerAuth, registerOnPostAuth: setupDeps.core.http.registerOnPostAuth, + registerOnPreResponse: setupDeps.core.http.registerOnPreResponse, basePath: setupDeps.core.http.basePath, isTlsEnabled: setupDeps.core.http.isTlsEnabled, }, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 8f864dda6b9f3..c07caaa04ba52 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -90,6 +90,7 @@ function createCoreSetupMock() { registerOnPreAuth: httpService.registerOnPreAuth, registerAuth: httpService.registerAuth, registerOnPostAuth: httpService.registerOnPostAuth, + registerOnPreResponse: httpService.registerOnPreResponse, basePath: httpService.basePath, isTlsEnabled: httpService.isTlsEnabled, createRouter: jest.fn(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index dfd1052bbec75..6829784e6e0a1 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -159,6 +159,7 @@ export function createPluginSetupContext( registerOnPreAuth: deps.http.registerOnPreAuth, registerAuth: deps.http.registerAuth, registerOnPostAuth: deps.http.registerOnPostAuth, + registerOnPreResponse: deps.http.registerOnPreResponse, basePath: deps.http.basePath, isTlsEnabled: deps.http.isTlsEnabled, }, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7e1226aa7238b..c855e04e420f7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -697,6 +697,7 @@ export interface HttpServiceSetup { registerAuth: (handler: AuthenticationHandler) => void; registerOnPostAuth: (handler: OnPostAuthHandler) => void; registerOnPreAuth: (handler: OnPreAuthHandler) => void; + registerOnPreResponse: (handler: OnPreResponseHandler) => void; registerRouteHandlerContext: (contextName: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer; } @@ -976,6 +977,27 @@ export interface OnPreAuthToolkit { rewriteUrl: (url: string) => OnPreAuthResult; } +// @public +export interface OnPreResponseExtensions { + headers?: ResponseHeaders; +} + +// Warning: (ae-forgotten-export) The symbol "OnPreResponseResult" needs to be exported by the entry point index.d.ts +// +// @public +export type OnPreResponseHandler = (request: KibanaRequest, preResponse: OnPreResponseInfo, toolkit: OnPreResponseToolkit) => OnPreResponseResult | Promise; + +// @public +export interface OnPreResponseInfo { + // (undocumented) + statusCode: number; +} + +// @public +export interface OnPreResponseToolkit { + next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; +} + // @public (undocumented) export interface PackageInfo { // (undocumented) From 20d30e5b27f2879c510a4832f79c1e45b02fa616 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 6 Dec 2019 17:42:45 +0000 Subject: [PATCH 13/35] chore(NA): add resolution to bump serialize-javascript (#52336) --- package.json | 3 ++- yarn.lock | 13 ++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index c3d19367e8b92..2b157da779f63 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ "**/image-diff/gm/debug": "^2.6.9", "**/react-dom": "^16.12.0", "**/react-test-renderer": "^16.12.0", - "**/deepmerge": "^4.2.2" + "**/deepmerge": "^4.2.2", + "**/serialize-javascript": "^2.1.1" }, "workspaces": { "packages": [ diff --git a/yarn.lock b/yarn.lock index 49216f9dac056..b4960a6cd01e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25355,15 +25355,10 @@ sentence-case@^2.1.0: no-case "^2.2.0" upper-case-first "^1.1.2" -serialize-javascript@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" - integrity sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA== - -serialize-javascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.0.tgz#9310276819efd0eb128258bb341957f6eb2fc570" - integrity sha512-a/mxFfU00QT88umAJQsNWOnUKckhNCqOl028N48e7wFmo2/EHpTo9Wso+iJJCMrQnmFvcjto5RJdAHEvVhcyUQ== +serialize-javascript@^1.7.0, serialize-javascript@^2.1.0, serialize-javascript@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.1.tgz#952907a04a3e3a75af7f73d92d15e233862048b2" + integrity sha512-MPLPRpD4FNqWq9tTIjYG5LesFouDhdyH0EPY3gVK4DRD5+g4aDqdNSzLIwceulo3Yj+PL1bPh6laE5+H6LTcrQ== serve-favicon@^2.5.0: version "2.5.0" From 3368ce096c5d3a57344767fbc728fea21dbff3dd Mon Sep 17 00:00:00 2001 From: Matt Bargar Date: Fri, 6 Dec 2019 13:04:26 -0500 Subject: [PATCH 14/35] Preserve currently loaded Saved Query in Discover when page reloads (#52323) * Fix import * Add test that would have failed with previous bug --- .../kibana/public/discover/angular/discover.js | 4 ++-- test/functional/apps/discover/_saved_queries.js | 12 ++++++++++++ .../services/saved_query_management_component.ts | 9 +++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js index 7abb7166aa902..ec0c5c34f7a93 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js @@ -79,7 +79,7 @@ import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { registerTimefilterWithGlobalStateFactory } from '../../../../../ui/public/timefilter/setup_router'; import { FilterStateManager } from '../../../../data/public/filter/filter_manager'; -const { savedQueryService } = data.query.savedQueries; +const { getSavedQuery } = data.query.savedQueries; const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -972,7 +972,7 @@ function discoverController( return; } if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) { - savedQueryService.getSavedQuery(newSavedQueryId).then((savedQuery) => { + getSavedQuery(newSavedQueryId).then((savedQuery) => { $scope.$evalAsync(() => { $scope.savedQuery = savedQuery; updateStateFromSavedQuery(savedQuery); diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 8fbc40f86e8dc..3ae8f51fb76dc 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const browser = getService('browser'); const defaultSettings = { defaultIndex: 'logstash-*', @@ -86,6 +87,17 @@ export default function ({ getService, getPageObjects }) { expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); }); + it('preserves the currently loaded query when the page is reloaded', async () => { + await browser.refresh(); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + expect(await PageObjects.discover.getHitCount()).to.be('2,792'); + expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse'); + }); + + it('allows saving changes to a currently loaded query via the saved query management component', async () => { await queryBar.setQuery('response:404'); await savedQueryManagementComponent.updateCurrentlyLoadedQuery( diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index d6de0be0c172e..9f0a8ded649b2 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -26,6 +26,15 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide const retry = getService('retry'); class SavedQueryManagementComponent { + public async getCurrentlyLoadedQueryID() { + await this.openSavedQueryManagementComponent(); + try { + return await testSubjects.getVisibleText('~saved-query-list-item-selected'); + } catch { + return undefined; + } + } + public async saveNewQuery( name: string, description: string, From ab5913d1092633035a3261669642f79f68881d47 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Fri, 6 Dec 2019 13:35:34 -0500 Subject: [PATCH 15/35] Infra server NP shim + config/routing API adoption (#45299) * Basic cleanup before refactoring for shim work * shim WIP * Removes the configuration adapter * WIP more stuff * WIP refactoring of shimming work * WIP continues * Logging UI now runs on top of new platform shim * WIP continues * Removes unused imports and variables * Basic infra NP server shim in place * Reimplemented graphql http error handling for infra NP server shim * Adds new platform infra plugin to handle NP config for legacy server shim * Basic cleanup before refactoring for shim work * shim WIP * Removes the configuration adapter * WIP more stuff * WIP refactoring of shimming work * WIP continues * Logging UI now runs on top of new platform shim * WIP continues * Removes unused imports and variables * Basic infra NP server shim in place * Reimplemented graphql http error handling for infra NP server shim * Adds new platform infra plugin to handle NP config for legacy server shim * Adds comment about duplicating full config for NP config * Use New Platform features plugin to registerFeature() * Re-arranging and relying on request context as uch as possible * Refactors KibanaRequest for RequestHandlerContext * fixes types for callWithRequest * Moves callWithRequest method override types directly into class to get them working, need to fix this when we understand it better * Fixes callWithRequest framework types * Removes a few NP_TODO comments * Fix broken imports * Ensure GraphQL resolvers are actually passed requestContext and not the raw request, and switch to the savedObjects client via requestContext * Remove the legacy traces of the savedObjects plugin * Fixes TSVB access with NP raw requests and requestContext * Remove unused getUiSettingsService (moved to requestContext) * Migrate to new Spaces plugin * Fix calculateMetricInterval after merged changes * Reinstate and migrate the infrastructure metadata route * Fix various type check errors * Amend InfraSources lib unit tests Mock the savedObjects client differently * Amend MetricsExplorer API response Renaming of variable inadvertently broke the response * Remove GraphQLI references from feature controls tests * Remove other GraphiQL references * Fix security / access issue * Add a framework level registerRoute method which always adds access tags by default * *Temp* disable test * Migrate the log rate validation endpoint to the new platform Fully migrates the [Logs UI] log rate setup index validation #50008 PR to New Platform routing etc * Amend types * Example of how to expose APM get indices method in NP * Fix calls to TSVB bug caused by object mutation This is a temp fix as the TSVB NP migration will supercede this * Converts getApmIndices function to accept saved object client, implements usage in infra * Fix APM setup_request tests * Fixes some unused references for linting * Migrate all work from #50730 to NP * Remove duplicate declaration files for rison_node and add a single source of truth at x-pack/typings/rison_node.d.ts for x-pack uses * Moved type file back into infra plugin to bypass strange break * Updates apm indices method signature per feedback from @elastic/apm-ui --- .../apm/server/lib/helpers/es_client.ts | 5 +- .../server/lib/helpers/setup_request.test.ts | 10 + .../apm/server/lib/helpers/setup_request.ts | 5 +- .../settings/apm_indices/get_apm_indices.ts | 18 +- .../apm/server/routes/settings/apm_indices.ts | 5 +- .../infra/common/http_api/metadata_api.ts | 4 +- .../infra/common/http_api/node_details_api.ts | 3 - .../infra/common/http_api/snapshot_api.ts | 2 - x-pack/legacy/plugins/infra/index.ts | 58 ++- .../framework/kibana_framework_adapter.ts | 2 +- .../public/lib/compose/kibana_compose.ts | 4 +- .../public/lib/compose/testing_compose.ts | 4 +- .../legacy/plugins/infra/server/features.ts | 65 +++ .../plugins/infra/server/infra_server.ts | 2 +- .../plugins/infra/server/kibana.index.ts | 92 +---- .../adapters/configuration/adapter_types.ts | 19 - .../lib/adapters/configuration/index.ts | 7 - .../inmemory_configuration_adapter.ts | 16 - .../kibana_configuration_adapter.test.ts | 40 -- .../kibana_configuration_adapter.ts | 73 ---- .../lib/adapters/fields/adapter_types.ts | 4 +- .../fields/framework_fields_adapter.ts | 26 +- .../lib/adapters/framework/adapter_types.ts | 141 ++----- .../adapters/framework/apollo_server_hapi.ts | 117 ------ .../framework/kibana_framework_adapter.ts | 377 +++++++++++------- .../log_entries/kibana_log_entries_adapter.ts | 27 +- .../lib/adapters/metrics/adapter_types.ts | 8 +- .../metrics/kibana_metrics_adapter.ts | 33 +- .../elasticsearch_source_status_adapter.ts | 24 +- .../infra/server/lib/compose/kibana.ts | 20 +- .../infra/server/lib/domains/fields_domain.ts | 11 +- .../log_entries_domain/log_entries_domain.ts | 65 +-- .../server/lib/domains/metrics_domain.ts | 9 +- .../plugins/infra/server/lib/infra_types.ts | 19 +- .../server/lib/log_analysis/log_analysis.ts | 15 +- .../infra/server/lib/snapshot/snapshot.ts | 30 +- .../plugins/infra/server/lib/source_status.ts | 71 +++- .../infra/server/lib/sources/sources.test.ts | 122 +++--- .../infra/server/lib/sources/sources.ts | 99 ++--- .../infra/server/new_platform_index.ts | 16 + .../infra/server/new_platform_plugin.ts | 107 +++++ .../infra/server/routes/ip_to_hostname.ts | 58 ++- .../log_analysis/index_patterns/validate.ts | 101 ++--- .../log_analysis/results/log_entry_rate.ts | 67 ++-- .../infra/server/routes/metadata/index.ts | 51 ++- .../metadata/lib/get_cloud_metric_metadata.ts | 10 +- .../metadata/lib/get_metric_metadata.ts | 10 +- .../routes/metadata/lib/get_node_info.ts | 16 +- .../routes/metadata/lib/get_pod_node_name.ts | 12 +- .../routes/metadata/lib/has_apm_data.ts | 20 +- .../server/routes/metrics_explorer/index.ts | 42 +- .../lib/create_metrics_model.ts | 4 +- .../metrics_explorer/lib/get_groupings.ts | 4 +- .../lib/populate_series_with_tsvb_data.ts | 26 +- .../server/routes/metrics_explorer/types.ts | 6 +- .../infra/server/routes/node_details/index.ts | 50 ++- .../infra/server/routes/snapshot/index.ts | 68 ++-- .../server/utils/calculate_metric_interval.ts | 13 +- .../server/utils/get_all_composite_data.ts | 20 +- x-pack/plugins/apm/server/plugin.ts | 17 +- x-pack/plugins/infra/kibana.json | 5 + x-pack/plugins/infra/server/index.ts | 24 ++ x-pack/plugins/infra/server/plugin.ts | 33 ++ .../apis/infra/feature_controls.ts | 41 -- x-pack/test/typings/rison_node.d.ts | 26 -- 65 files changed, 1263 insertions(+), 1236 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/server/features.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts create mode 100644 x-pack/legacy/plugins/infra/server/new_platform_index.ts create mode 100644 x-pack/legacy/plugins/infra/server/new_platform_plugin.ts create mode 100644 x-pack/plugins/infra/kibana.json create mode 100644 x-pack/plugins/infra/server/index.ts create mode 100644 x-pack/plugins/infra/server/plugin.ts delete mode 100644 x-pack/test/typings/rison_node.d.ts diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts index 28035ac2f9be2..c2dce4f4638ae 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts @@ -78,7 +78,10 @@ async function getParamsForSearchRequest( ) { const { uiSettings } = context.core; const [indices, includeFrozen] = await Promise.all([ - getApmIndices(context), + getApmIndices({ + savedObjectsClient: context.core.savedObjects.client, + config: context.config + }), uiSettings.client.get('search:includeFrozen') ]); diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts index f320712d6151f..4272bdbddd26b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -50,6 +50,11 @@ function getMockRequest() { client: { get: jest.fn().mockResolvedValue(false) } + }, + savedObjects: { + client: { + get: jest.fn() + } } } } as unknown) as APMRequestHandlerContext & { @@ -65,6 +70,11 @@ function getMockRequest() { get: jest.Mock; }; }; + savedObjects: { + client: { + get: jest.Mock; + }; + }; }; }; diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index a09cdbf91ec6e..56c9255844009 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -73,7 +73,10 @@ export async function setupRequest( const { config } = context; const { query } = context.params; - const indices = await getApmIndices(context); + const indices = await getApmIndices({ + savedObjectsClient: context.core.savedObjects.client, + config + }); const dynamicIndexPattern = await getDynamicIndexPattern({ context, diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts index 0ed30ec4cdd27..e451f89af5620 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts @@ -54,15 +54,25 @@ export function getApmIndicesConfig(config: APMConfig): ApmIndicesConfig { }; } -export async function getApmIndices(context: APMRequestHandlerContext) { +// export async function getApmIndices(context: APMRequestHandlerContext) { +// return _getApmIndices(context.core, context.config); +// } + +export async function getApmIndices({ + config, + savedObjectsClient +}: { + config: APMConfig; + savedObjectsClient: SavedObjectsClientContract; +}) { try { const apmIndicesSavedObject = await getApmIndicesSavedObject( - context.core.savedObjects.client + savedObjectsClient ); - const apmIndicesConfig = getApmIndicesConfig(context.config); + const apmIndicesConfig = getApmIndicesConfig(config); return merge({}, apmIndicesConfig, apmIndicesSavedObject); } catch (error) { - return getApmIndicesConfig(context.config); + return getApmIndicesConfig(config); } } diff --git a/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts index b66eb05f6eda5..a69fba52be3f0 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts @@ -26,7 +26,10 @@ export const apmIndicesRoute = createRoute(() => ({ method: 'GET', path: '/api/apm/settings/apm-indices', handler: async ({ context }) => { - return await getApmIndices(context); + return await getApmIndices({ + savedObjectsClient: context.core.savedObjects.client, + config: context.config + }); } })); diff --git a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts index 5b9389a073002..ace61e13193c8 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts @@ -5,7 +5,6 @@ */ import * as rt from 'io-ts'; -import { InfraWrappableRequest } from '../../server/lib/adapters/framework'; export const InfraMetadataNodeTypeRT = rt.keyof({ host: null, @@ -67,6 +66,7 @@ export const InfraMetadataInfoRT = rt.partial({ }); const InfraMetadataRequiredRT = rt.type({ + id: rt.string, name: rt.string, features: rt.array(InfraMetadataFeatureRT), }); @@ -81,8 +81,6 @@ export type InfraMetadata = rt.TypeOf; export type InfraMetadataRequest = rt.TypeOf; -export type InfraMetadataWrappedRequest = InfraWrappableRequest; - export type InfraMetadataFeature = rt.TypeOf; export type InfraMetadataInfo = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts b/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts index 607d71654032e..46aab881bce4c 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts @@ -6,7 +6,6 @@ import * as rt from 'io-ts'; import { InventoryMetricRT, ItemTypeRT } from '../inventory_models/types'; -import { InfraWrappableRequest } from '../../server/lib/adapters/framework'; import { InfraTimerangeInputRT } from './snapshot_api'; const NodeDetailsDataPointRT = rt.intersection([ @@ -53,6 +52,4 @@ export const NodeDetailsRequestRT = rt.intersection([ // export type NodeDetailsRequest = InfraWrappableRequest; export type NodeDetailsRequest = rt.TypeOf; -export type NodeDetailsWrappedRequest = InfraWrappableRequest; - export type NodeDetailsMetricDataResponse = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts index 24ca0fed73338..3e6aec4bad972 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts @@ -5,7 +5,6 @@ */ import * as rt from 'io-ts'; -import { InfraWrappableRequest } from '../../server/lib/adapters/framework'; import { SnapshotMetricTypeRT, ItemTypeRT } from '../inventory_models/types'; export const SnapshotNodePathRT = rt.intersection([ @@ -64,6 +63,5 @@ export const SnapshotRequestRT = rt.intersection([ ]); export type SnapshotRequest = rt.TypeOf; -export type SnapshotWrappedRequest = InfraWrappableRequest; export type SnapshotNode = rt.TypeOf; export type SnapshotNodeResponse = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/index.ts b/x-pack/legacy/plugins/infra/index.ts index 9bf679fb5ff80..dbf1f4ad61de3 100644 --- a/x-pack/legacy/plugins/infra/index.ts +++ b/x-pack/legacy/plugins/infra/index.ts @@ -7,9 +7,14 @@ import { i18n } from '@kbn/i18n'; import JoiNamespace from 'joi'; import { resolve } from 'path'; - -import { getConfigSchema, initServerWithKibana } from './server/kibana.index'; +import { PluginInitializerContext } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import KbnServer from 'src/legacy/server/kbn_server'; +import { getConfigSchema } from './server/kibana.index'; import { savedObjectMappings } from './server/saved_objects'; +import { plugin, InfraServerPluginDeps } from './server/new_platform_index'; +import { InfraSetup } from '../../../plugins/infra/server'; +import { APMPluginContract } from '../../../plugins/apm/server/plugin'; const APP_ID = 'infra'; const logsSampleDataLinkLabel = i18n.translate('xpack.infra.sampleDataLinkLabel', { @@ -70,9 +75,52 @@ export function infra(kibana: any) { config(Joi: typeof JoiNamespace) { return getConfigSchema(Joi); }, - init(server: any) { - initServerWithKibana(server); - server.addAppLinksToSampleDataset('logs', [ + init(legacyServer: any) { + const { newPlatform } = legacyServer as KbnServer; + const { core, plugins } = newPlatform.setup; + + const infraSetup = (plugins.infra as unknown) as InfraSetup; // chef's kiss + + const initContext = ({ + config: infraSetup.__legacy.config, + } as unknown) as PluginInitializerContext; + // NP_TODO: Use real types from the other plugins as they are migrated + const pluginDeps: InfraServerPluginDeps = { + usageCollection: plugins.usageCollection as UsageCollectionSetup, + indexPatterns: { + indexPatternsServiceFactory: legacyServer.indexPatternsServiceFactory, + }, + metrics: legacyServer.plugins.metrics, + spaces: plugins.spaces, + features: plugins.features, + // NP_NOTE: [TSVB_GROUP] Huge hack to make TSVB (getVisData()) work with raw requests that + // originate from the New Platform router (and are very different to the old request object). + // Once TSVB has migrated over to NP, and can work with the new raw requests, or ideally just + // the requestContext, this can be removed. + ___legacy: { + tsvb: { + elasticsearch: legacyServer.plugins.elasticsearch, + __internals: legacyServer.newPlatform.__internals, + }, + }, + apm: plugins.apm as APMPluginContract, + }; + + const infraPluginInstance = plugin(initContext); + infraPluginInstance.setup(core, pluginDeps); + + // NP_TODO: EVERYTHING BELOW HERE IS LEGACY + + const libs = infraPluginInstance.getLibs(); + + // NP_TODO how do we replace this? Answer: return from setup function. + legacyServer.expose( + 'defineInternalSourceConfiguration', + libs.sources.defineInternalSourceConfiguration.bind(libs.sources) + ); + + // NP_TODO: How do we move this to new platform? + legacyServer.addAppLinksToSampleDataset('logs', [ { path: `/app/${APP_ID}#/logs`, label: logsSampleDataLinkLabel, diff --git a/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts index d70a42473b710..f91b40815a3ae 100644 --- a/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -24,7 +24,7 @@ import { const ROOT_ELEMENT_ID = 'react-infra-root'; const BREADCRUMBS_ELEMENT_ID = 'react-infra-breadcrumbs'; -export class InfraKibanaFrameworkAdapter implements InfraFrameworkAdapter { +export class KibanaFramework implements InfraFrameworkAdapter { public appState: object; public kbnVersion?: string; public timezone?: string; diff --git a/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts b/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts index 086691e665b03..9b0beb3ad519c 100644 --- a/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts +++ b/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts @@ -20,7 +20,7 @@ import { HttpLink } from 'apollo-link-http'; import { withClientState } from 'apollo-link-state'; import { InfraFrontendLibs } from '../lib'; import introspectionQueryResultData from '../../graphql/introspection.json'; -import { InfraKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api'; export function compose(): InfraFrontendLibs { @@ -57,7 +57,7 @@ export function compose(): InfraFrontendLibs { const infraModule = uiModules.get('app/infa'); - const framework = new InfraKibanaFrameworkAdapter(infraModule, uiRoutes, timezoneProvider); + const framework = new KibanaFramework(infraModule, uiRoutes, timezoneProvider); const libs: InfraFrontendLibs = { apolloClient, diff --git a/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts b/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts index 14fd66d378121..1e0b2f079497d 100644 --- a/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts +++ b/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts @@ -17,7 +17,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory'; import ApolloClient from 'apollo-client'; import { SchemaLink } from 'apollo-link-schema'; import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools'; -import { InfraKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api'; import { InfraFrontendLibs } from '../lib'; @@ -27,7 +27,7 @@ export function compose(): InfraFrontendLibs { basePath: chrome.getBasePath(), xsrfToken: chrome.getXsrfToken(), }); - const framework = new InfraKibanaFrameworkAdapter(infraModule, uiRoutes, timezoneProvider); + const framework = new KibanaFramework(infraModule, uiRoutes, timezoneProvider); const typeDefs = ` Query {} `; diff --git a/x-pack/legacy/plugins/infra/server/features.ts b/x-pack/legacy/plugins/infra/server/features.ts new file mode 100644 index 0000000000000..fc20813c777b6 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/features.ts @@ -0,0 +1,65 @@ +/* + * 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'; + +export const METRICS_FEATURE = { + id: 'infrastructure', + name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { + defaultMessage: 'Infrastructure', + }), + icon: 'infraApp', + navLinkId: 'infra:home', + app: ['infra', 'kibana'], + catalogue: ['infraops'], + privileges: { + all: { + api: ['infra'], + savedObject: { + all: ['infrastructure-ui-source'], + read: ['index-pattern'], + }, + ui: ['show', 'configureSource', 'save'], + }, + read: { + api: ['infra'], + savedObject: { + all: [], + read: ['infrastructure-ui-source', 'index-pattern'], + }, + ui: ['show'], + }, + }, +}; + +export const LOGS_FEATURE = { + id: 'logs', + name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { + defaultMessage: 'Logs', + }), + icon: 'loggingApp', + navLinkId: 'infra:logs', + app: ['infra', 'kibana'], + catalogue: ['infralogging'], + privileges: { + all: { + api: ['infra'], + savedObject: { + all: ['infrastructure-ui-source'], + read: [], + }, + ui: ['show', 'configureSource', 'save'], + }, + read: { + api: ['infra'], + savedObject: { + all: [], + read: ['infrastructure-ui-source'], + }, + ui: ['show'], + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts index edccf5f413ab4..845e54e18c7c5 100644 --- a/x-pack/legacy/plugins/infra/server/infra_server.ts +++ b/x-pack/legacy/plugins/infra/server/infra_server.ts @@ -30,7 +30,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { typeDefs: schemas, }); - libs.framework.registerGraphQLEndpoint('/api/infra/graphql', schema); + libs.framework.registerGraphQLEndpoint('/graphql', schema); initIpToHostName(libs); initLogAnalysisGetLogEntryRateRoute(libs); diff --git a/x-pack/legacy/plugins/infra/server/kibana.index.ts b/x-pack/legacy/plugins/infra/server/kibana.index.ts index 91bcd6be95a75..b4301b3edf367 100644 --- a/x-pack/legacy/plugins/infra/server/kibana.index.ts +++ b/x-pack/legacy/plugins/infra/server/kibana.index.ts @@ -4,97 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { Server } from 'hapi'; import JoiNamespace from 'joi'; -import { initInfraServer } from './infra_server'; -import { compose } from './lib/compose/kibana'; -import { UsageCollector } from './usage/usage_collector'; -import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; -import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; -export const initServerWithKibana = (kbnServer: Server) => { - const { usageCollection } = kbnServer.newPlatform.setup.plugins; - const libs = compose(kbnServer); - initInfraServer(libs); - - kbnServer.expose( - 'defineInternalSourceConfiguration', - libs.sources.defineInternalSourceConfiguration.bind(libs.sources) - ); - - // Register a function with server to manage the collection of usage stats - UsageCollector.registerUsageCollector(usageCollection); - - const xpackMainPlugin = kbnServer.plugins.xpack_main; - xpackMainPlugin.registerFeature({ - id: 'infrastructure', - name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { - defaultMessage: 'Metrics', - }), - icon: 'metricsApp', - navLinkId: 'infra:home', - app: ['infra', 'kibana'], - catalogue: ['infraops'], - privileges: { - all: { - api: ['infra'], - savedObject: { - all: [ - 'infrastructure-ui-source', - inventoryViewSavedObjectType, - metricsExplorerViewSavedObjectType, - ], - read: ['index-pattern'], - }, - ui: ['show', 'configureSource', 'save'], - }, - read: { - api: ['infra'], - savedObject: { - all: [], - read: [ - 'infrastructure-ui-source', - 'index-pattern', - inventoryViewSavedObjectType, - metricsExplorerViewSavedObjectType, - ], - }, - ui: ['show'], - }, - }, - }); - - xpackMainPlugin.registerFeature({ - id: 'logs', - name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { - defaultMessage: 'Logs', - }), - icon: 'logsApp', - navLinkId: 'infra:logs', - app: ['infra', 'kibana'], - catalogue: ['infralogging'], - privileges: { - all: { - api: ['infra'], - savedObject: { - all: ['infrastructure-ui-source'], - read: [], - }, - ui: ['show', 'configureSource', 'save'], - }, - read: { - api: ['infra'], - savedObject: { - all: [], - read: ['infrastructure-ui-source'], - }, - ui: ['show'], - }, - }, - }); -}; +export interface KbnServer extends Server { + usage: any; +} +// NP_TODO: this is only used in the root index file AFAICT, can remove after migrating to NP export const getConfigSchema = (Joi: typeof JoiNamespace) => { const InfraDefaultSourceConfigSchema = Joi.object({ metricAlias: Joi.string(), @@ -111,6 +28,7 @@ export const getConfigSchema = (Joi: typeof JoiNamespace) => { }), }); + // NP_TODO: make sure this is all represented in the NP config schema const InfraRootConfigSchema = Joi.object({ enabled: Joi.boolean().default(true), query: Joi.object({ diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts deleted file mode 100644 index b0856cf3da361..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 interface InfraConfigurationAdapter< - Configuration extends InfraBaseConfiguration = InfraBaseConfiguration -> { - get(): Promise; -} - -export interface InfraBaseConfiguration { - enabled: boolean; - query: { - partitionSize: number; - partitionFactor: number; - }; -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts deleted file mode 100644 index 4e09b5d0e9e2d..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * 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 './adapter_types'; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts deleted file mode 100644 index 472fa72939565..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 { InfraBaseConfiguration, InfraConfigurationAdapter } from './adapter_types'; - -export class InfraInmemoryConfigurationAdapter - implements InfraConfigurationAdapter { - constructor(private readonly configuration: Configuration) {} - - public async get() { - return this.configuration; - } -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts deleted file mode 100644 index 4d87878e9aa87..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { InfraKibanaConfigurationAdapter } from './kibana_configuration_adapter'; - -describe('the InfraKibanaConfigurationAdapter', () => { - test('queries the xpack.infra configuration of the server', async () => { - const mockConfig = { - get: jest.fn(), - }; - - const configurationAdapter = new InfraKibanaConfigurationAdapter({ - config: () => mockConfig, - }); - - await configurationAdapter.get(); - - expect(mockConfig.get).toBeCalledWith('xpack.infra'); - }); - - test('applies the query defaults', async () => { - const configurationAdapter = new InfraKibanaConfigurationAdapter({ - config: () => ({ - get: () => ({}), - }), - }); - - const configuration = await configurationAdapter.get(); - - expect(configuration).toMatchObject({ - query: { - partitionSize: expect.any(Number), - partitionFactor: expect.any(Number), - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts deleted file mode 100644 index d3699a4820cf0..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 Joi from 'joi'; - -import { InfraBaseConfiguration, InfraConfigurationAdapter } from './adapter_types'; - -export class InfraKibanaConfigurationAdapter implements InfraConfigurationAdapter { - private readonly server: ServerWithConfig; - - constructor(server: any) { - if (!isServerWithConfig(server)) { - throw new Error('Failed to find configuration on server.'); - } - - this.server = server; - } - - public async get() { - const config = this.server.config(); - - if (!isKibanaConfiguration(config)) { - throw new Error('Failed to access configuration of server.'); - } - - const configuration = config.get('xpack.infra') || {}; - const configurationWithDefaults: InfraBaseConfiguration = { - enabled: true, - query: { - partitionSize: 75, - partitionFactor: 1.2, - ...(configuration.query || {}), - }, - ...configuration, - }; - - // we assume this to be the configuration because Kibana would have already validated it - return configurationWithDefaults; - } -} - -interface ServerWithConfig { - config(): any; -} - -function isServerWithConfig(maybeServer: any): maybeServer is ServerWithConfig { - return ( - Joi.validate( - maybeServer, - Joi.object({ - config: Joi.func().required(), - }).unknown() - ).error === null - ); -} - -interface KibanaConfiguration { - get(key: string): any; -} - -function isKibanaConfiguration(maybeConfiguration: any): maybeConfiguration is KibanaConfiguration { - return ( - Joi.validate( - maybeConfiguration, - Joi.object({ - get: Joi.func().required(), - }).unknown() - ).error === null - ); -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts index 66081e60e7e10..3aaa23b378096 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraFrameworkRequest } from '../framework'; +import { RequestHandlerContext } from 'src/core/server'; export interface FieldsAdapter { getIndexFields( - req: InfraFrameworkRequest, + requestContext: RequestHandlerContext, indices: string, timefield: string ): Promise; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts index a6881a05f6f93..01306901e9caa 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts @@ -6,11 +6,9 @@ import { startsWith, uniq, first } from 'lodash'; import { idx } from '@kbn/elastic-idx'; -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraDatabaseSearchResponse, -} from '../framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { InfraDatabaseSearchResponse } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { FieldsAdapter, IndexFieldDescriptor } from './adapter_types'; import { getAllowedListForPrefix } from '../../../../common/ecs_allowed_list'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; @@ -31,22 +29,26 @@ interface DataSetResponse { } export class FrameworkFieldsAdapter implements FieldsAdapter { - private framework: InfraBackendFrameworkAdapter; + private framework: KibanaFramework; - constructor(framework: InfraBackendFrameworkAdapter) { + constructor(framework: KibanaFramework) { this.framework = framework; } public async getIndexFields( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, indices: string, timefield: string ): Promise { - const indexPatternsService = this.framework.getIndexPatternsService(request); + const indexPatternsService = this.framework.getIndexPatternsService(requestContext); const response = await indexPatternsService.getFieldsForWildcard({ pattern: indices, }); - const { dataSets, modules } = await this.getDataSetsAndModules(request, indices, timefield); + const { dataSets, modules } = await this.getDataSetsAndModules( + requestContext, + indices, + timefield + ); const allowedList = modules.reduce( (acc, name) => uniq([...acc, ...getAllowedListForPrefix(name)]), [] as string[] @@ -59,7 +61,7 @@ export class FrameworkFieldsAdapter implements FieldsAdapter { } private async getDataSetsAndModules( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, indices: string, timefield: string ): Promise<{ dataSets: string[]; modules: string[] }> { @@ -109,7 +111,7 @@ export class FrameworkFieldsAdapter implements FieldsAdapter { const buckets = await getAllCompositeData( this.framework, - request, + requestContext, params, bucketSelector, handleAfterKey diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 63fded49d8222..625607c098028 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -4,91 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; -import { GraphQLSchema } from 'graphql'; -import { Lifecycle, ResponseToolkit, RouteOptions } from 'hapi'; -import { Legacy } from 'kibana'; - -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { JsonObject } from '../../../../common/typed_json'; -import { TSVBMetricModel } from '../../../../common/inventory_models/types'; - -export const internalInfraFrameworkRequest = Symbol('internalInfraFrameworkRequest'); - -/* eslint-disable @typescript-eslint/unified-signatures */ -export interface InfraBackendFrameworkAdapter { - version: string; - exposeStaticDir(urlPath: string, dir: string): void; - registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; - registerRoute( - route: InfraFrameworkRouteOptions - ): void; - callWithRequest( - req: InfraFrameworkRequest, - method: 'search', - options?: object - ): Promise>; - callWithRequest( - req: InfraFrameworkRequest, - method: 'msearch', - options?: object - ): Promise>; - callWithRequest( - req: InfraFrameworkRequest, - method: 'fieldCaps', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'indices.existsAlias', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'indices.getAlias', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'indices.get', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'ml.getBuckets', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: string, - options?: object - ): Promise; - getIndexPatternsService(req: InfraFrameworkRequest): Legacy.IndexPatternsService; - getSavedObjectsService(): Legacy.SavedObjectsService; - getSpaceId(request: InfraFrameworkRequest): string; - makeTSVBRequest( - req: InfraFrameworkRequest, - model: TSVBMetricModel, - timerange: { min: number; max: number }, - filters: JsonObject[] - ): Promise; - config(req: InfraFrameworkRequest): KibanaConfig; -} -/* eslint-enable @typescript-eslint/unified-signatures */ - -export interface InfraFrameworkRequest< - InternalRequest extends InfraWrappableRequest = InfraWrappableRequest -> { - [internalInfraFrameworkRequest]: InternalRequest; - payload: InternalRequest['payload']; - params: InternalRequest['params']; - query: InternalRequest['query']; -} - -export interface InfraWrappableRequest { - payload: Payload; - params: Params; - query: Query; +import { SearchResponse, GenericParams } from 'elasticsearch'; +import { Lifecycle } from 'hapi'; +import { ObjectType } from '@kbn/config-schema'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { RouteMethod, RouteConfig } from '../../../../../../../../src/core/server'; +import { APMPluginContract } from '../../../../../../../plugins/apm/server/plugin'; + +// NP_TODO: Compose real types from plugins we depend on, no "any" +export interface InfraServerPluginDeps { + usageCollection: UsageCollectionSetup; + spaces: any; + metrics: { + getVisData: any; + }; + indexPatterns: { + indexPatternsServiceFactory: any; + }; + features: any; + apm: APMPluginContract; + ___legacy: any; +} + +export interface CallWithRequestParams extends GenericParams { + max_concurrent_shard_requests?: number; + name?: string; + index?: string; + ignore_unavailable?: boolean; + allow_no_indices?: boolean; + size?: number; + terminate_after?: number; + fields?: string; } export type InfraResponse = Lifecycle.ReturnValue; @@ -98,22 +44,6 @@ export interface InfraFrameworkPluginOptions { options: any; } -export interface InfraFrameworkRouteOptions< - RouteRequest extends InfraWrappableRequest, - RouteResponse extends InfraResponse -> { - path: string; - method: string | string[]; - vhost?: string; - handler: InfraFrameworkRouteHandler; - options?: Pick>; -} - -export type InfraFrameworkRouteHandler< - RouteRequest extends InfraWrappableRequest, - RouteResponse extends InfraResponse -> = (request: InfraFrameworkRequest, h: ResponseToolkit) => RouteResponse; - export interface InfraDatabaseResponse { took: number; timeout: boolean; @@ -235,3 +165,12 @@ export interface InfraTSVBSeries { } export type InfraTSVBDataPoint = [number, number]; + +export type InfraRouteConfig< + params extends ObjectType, + query extends ObjectType, + body extends ObjectType, + method extends RouteMethod +> = { + method: RouteMethod; +} & RouteConfig; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts deleted file mode 100644 index da858217468f1..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 GraphiQL from 'apollo-server-module-graphiql'; -import Boom from 'boom'; -import { Plugin, Request, ResponseToolkit, RouteOptions, Server } from 'hapi'; - -import { GraphQLOptions, runHttpQuery } from 'apollo-server-core'; - -export type HapiOptionsFunction = (req: Request) => GraphQLOptions | Promise; - -export interface HapiGraphQLPluginOptions { - path: string; - vhost?: string; - route?: RouteOptions; - graphqlOptions: GraphQLOptions | HapiOptionsFunction; -} - -export const graphqlHapi: Plugin = { - name: 'graphql', - register: (server: Server, options: HapiGraphQLPluginOptions) => { - if (!options || !options.graphqlOptions) { - throw new Error('Apollo Server requires options.'); - } - - server.route({ - options: options.route || {}, - handler: async (request: Request, h: ResponseToolkit) => { - try { - const query = - request.method === 'post' - ? (request.payload as Record) - : (request.query as Record); - - const gqlResponse = await runHttpQuery([request], { - method: request.method.toUpperCase(), - options: options.graphqlOptions, - query, - }); - - return h.response(gqlResponse).type('application/json'); - } catch (error) { - if ('HttpQueryError' !== error.name) { - const queryError = Boom.boomify(error); - - queryError.output.payload.message = error.message; - - return queryError; - } - - if (error.isGraphQLError === true) { - return h - .response(error.message) - .code(error.statusCode) - .type('application/json'); - } - - const genericError = new Boom(error.message, { statusCode: error.statusCode }); - - if (error.headers) { - Object.keys(error.headers).forEach(header => { - genericError.output.headers[header] = error.headers[header]; - }); - } - - // Boom hides the error when status code is 500 - - genericError.output.payload.message = error.message; - - throw genericError; - } - }, - method: ['GET', 'POST'], - path: options.path || '/graphql', - vhost: options.vhost || undefined, - }); - }, -}; - -export type HapiGraphiQLOptionsFunction = ( - req?: Request -) => GraphiQL.GraphiQLData | Promise; - -export interface HapiGraphiQLPluginOptions { - path: string; - - route?: any; - - graphiqlOptions: GraphiQL.GraphiQLData | HapiGraphiQLOptionsFunction; -} - -export const graphiqlHapi: Plugin = { - name: 'graphiql', - register: (server: Server, options: HapiGraphiQLPluginOptions) => { - if (!options || !options.graphiqlOptions) { - throw new Error('Apollo Server GraphiQL requires options.'); - } - - server.route({ - options: options.route || {}, - handler: async (request: Request, h: ResponseToolkit) => { - const graphiqlString = await GraphiQL.resolveGraphiQLString( - request.query, - options.graphiqlOptions, - request - ); - - return h.response(graphiqlString).type('text/html'); - }, - method: 'GET', - path: options.path || '/graphiql', - }); - }, -}; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index e96f1687bbb2e..19121d92f02c9 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -4,116 +4,207 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/array-type */ + import { GenericParams } from 'elasticsearch'; import { GraphQLSchema } from 'graphql'; import { Legacy } from 'kibana'; - -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { get } from 'lodash'; +import { runHttpQuery } from 'apollo-server-core'; +import { schema, TypeOf, ObjectType } from '@kbn/config-schema'; import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraFrameworkRouteOptions, - InfraResponse, + InfraRouteConfig, InfraTSVBResponse, - InfraWrappableRequest, - internalInfraFrameworkRequest, + InfraServerPluginDeps, + CallWithRequestParams, + InfraDatabaseSearchResponse, + InfraDatabaseMultiResponse, + InfraDatabaseFieldCapsResponse, + InfraDatabaseGetIndicesResponse, + InfraDatabaseGetIndicesAliasResponse, } from './adapter_types'; -import { - graphiqlHapi, - graphqlHapi, - HapiGraphiQLPluginOptions, - HapiGraphQLPluginOptions, -} from './apollo_server_hapi'; import { TSVBMetricModel } from '../../../../common/inventory_models/types'; +import { + CoreSetup, + IRouter, + KibanaRequest, + RequestHandlerContext, + KibanaResponseFactory, + RouteMethod, +} from '../../../../../../../../src/core/server'; +import { RequestHandler } from '../../../../../../../../src/core/server'; +import { InfraConfig } from '../../../../../../../plugins/infra/server'; -interface CallWithRequestParams extends GenericParams { - max_concurrent_shard_requests?: number; -} - -export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFrameworkAdapter { - public version: string; +export class KibanaFramework { + public router: IRouter; + private core: CoreSetup; + public plugins: InfraServerPluginDeps; - constructor(private server: Legacy.Server) { - this.version = server.config().get('pkg.version'); + constructor(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) { + this.router = core.http.createRouter(); + this.core = core; + this.plugins = plugins; } - public config(req: InfraFrameworkRequest): KibanaConfig { - const internalRequest = req[internalInfraFrameworkRequest]; - return internalRequest.server.config(); + public registerRoute< + params extends ObjectType = any, + query extends ObjectType = any, + body extends ObjectType = any, + method extends RouteMethod = any + >( + config: InfraRouteConfig, + handler: RequestHandler + ) { + const defaultOptions = { + tags: ['access:infra'], + }; + const routeConfig = { + path: config.path, + validate: config.validate, + // Currently we have no use of custom options beyond tags, this can be extended + // beyond defaultOptions if it's needed. + options: defaultOptions, + }; + switch (config.method) { + case 'get': + this.router.get(routeConfig, handler); + break; + case 'post': + this.router.post(routeConfig, handler); + break; + case 'delete': + this.router.delete(routeConfig, handler); + break; + case 'put': + this.router.put(routeConfig, handler); + break; + } } - public exposeStaticDir(urlPath: string, dir: string): void { - this.server.route({ - handler: { - directory: { - path: dir, - }, - }, - method: 'GET', - path: urlPath, - }); - } + public registerGraphQLEndpoint(routePath: string, gqlSchema: GraphQLSchema) { + // These endpoints are validated by GraphQL at runtime and with GraphQL generated types + const body = schema.object({}, { allowUnknowns: true }); + type Body = TypeOf; - public registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void { - this.server.register({ - options: { - graphqlOptions: (req: Legacy.Request) => ({ - context: { req: wrapRequest(req) }, - schema, - }), - path: routePath, - route: { - tags: ['access:infra'], - }, + const routeOptions = { + path: `/api/infra${routePath}`, + validate: { + body, }, - plugin: graphqlHapi, - }); - - this.server.register({ options: { - graphiqlOptions: request => ({ - endpointURL: request ? `${request.getBasePath()}${routePath}` : routePath, - passHeader: `'kbn-version': '${this.version}'`, - }), - path: `${routePath}/graphiql`, - route: { - tags: ['access:infra'], - }, + tags: ['access:infra'], }, - plugin: graphiqlHapi, - }); - } + }; + async function handler( + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + try { + const query = + request.route.method === 'post' + ? (request.body as Record) + : (request.query as Record); - public registerRoute< - RouteRequest extends InfraWrappableRequest, - RouteResponse extends InfraResponse - >(route: InfraFrameworkRouteOptions) { - const wrappedHandler = (request: any, h: Legacy.ResponseToolkit) => - route.handler(wrapRequest(request), h); - - this.server.route({ - handler: wrappedHandler, - options: route.options, - method: route.method, - path: route.path, - }); + const gqlResponse = await runHttpQuery([context, request], { + method: request.route.method.toUpperCase(), + options: (req: RequestHandlerContext, rawReq: KibanaRequest) => ({ + context: { req, rawReq }, + schema: gqlSchema, + }), + query, + }); + + return response.ok({ + body: gqlResponse, + headers: { + 'content-type': 'application/json', + }, + }); + } catch (error) { + const errorBody = { + message: error.message, + }; + + if ('HttpQueryError' !== error.name) { + return response.internalError({ + body: errorBody, + }); + } + + if (error.isGraphQLError === true) { + return response.customError({ + statusCode: error.statusCode, + body: errorBody, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + const { headers = [], statusCode = 500 } = error; + return response.customError({ + statusCode, + headers, + body: errorBody, + }); + + // NP_TODO: Do we still need to re-throw this error in this case? if we do, can we + // still call the response.customError method to control the HTTP response? + // throw error; + } + } + this.router.post(routeOptions, handler); + this.router.get(routeOptions, handler); } - public async callWithRequest( - req: InfraFrameworkRequest, + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'search', + options?: CallWithRequestParams + ): Promise>; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'msearch', + options?: CallWithRequestParams + ): Promise>; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'fieldCaps', + options?: CallWithRequestParams + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'indices.existsAlias', + options?: CallWithRequestParams + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + method: 'indices.getAlias', + options?: object + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + method: 'indices.get' | 'ml.getBuckets', + options?: object + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: string, + options?: CallWithRequestParams + ): Promise; + + public async callWithRequest( + requestContext: RequestHandlerContext, endpoint: string, - params: CallWithRequestParams, - ...rest: any[] + params: CallWithRequestParams ) { - const internalRequest = req[internalInfraFrameworkRequest]; - const { elasticsearch } = internalRequest.server.plugins; - const { callWithRequest } = elasticsearch.getCluster('data'); - const includeFrozen = await internalRequest.getUiSettingsService().get('search:includeFrozen'); + const { elasticsearch, uiSettings } = requestContext.core; + + const includeFrozen = await uiSettings.client.get('search:includeFrozen'); if (endpoint === 'msearch') { - const maxConcurrentShardRequests = await internalRequest - .getUiSettingsService() - .get('courier:maxConcurrentShardRequests'); + const maxConcurrentShardRequests = await uiSettings.client.get( + 'courier:maxConcurrentShardRequests' + ); if (maxConcurrentShardRequests > 0) { params = { ...params, max_concurrent_shard_requests: maxConcurrentShardRequests }; } @@ -125,95 +216,79 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework } : {}; - const fields = await callWithRequest( - internalRequest, - endpoint, - { - ...params, - ...frozenIndicesParams, - }, - ...rest - ); - return fields; + return elasticsearch.dataClient.callAsCurrentUser(endpoint, { + ...params, + ...frozenIndicesParams, + }); } public getIndexPatternsService( - request: InfraFrameworkRequest + requestContext: RequestHandlerContext ): Legacy.IndexPatternsService { - return this.server.indexPatternsServiceFactory({ + return this.plugins.indexPatterns.indexPatternsServiceFactory({ callCluster: async (method: string, args: [GenericParams], ...rest: any[]) => { - const fieldCaps = await this.callWithRequest( - request, - method, - { ...args, allowNoIndices: true } as GenericParams, - ...rest - ); + const fieldCaps = await this.callWithRequest(requestContext, method, { + ...args, + allowNoIndices: true, + } as GenericParams); return fieldCaps; }, }); } - public getSpaceId(request: InfraFrameworkRequest): string { - const spacesPlugin = this.server.plugins.spaces; + public getSpaceId(request: KibanaRequest): string { + const spacesPlugin = this.plugins.spaces; - if (spacesPlugin && typeof spacesPlugin.getSpaceId === 'function') { - return spacesPlugin.getSpaceId(request[internalInfraFrameworkRequest]); + if ( + spacesPlugin && + spacesPlugin.spacesService && + typeof spacesPlugin.spacesService.getSpaceId === 'function' + ) { + return spacesPlugin.spacesService.getSpaceId(request); } else { return 'default'; } } - public getSavedObjectsService() { - return this.server.savedObjects; - } - + // NP_TODO: This method needs to no longer require full KibanaRequest public async makeTSVBRequest( - req: InfraFrameworkRequest, + request: KibanaRequest, model: TSVBMetricModel, timerange: { min: number; max: number }, - filters: any[] - ) { - const internalRequest = req[internalInfraFrameworkRequest]; - const server = internalRequest.server; - const getVisData = get(server, 'plugins.metrics.getVisData'); + filters: any[], + requestContext: RequestHandlerContext + ): Promise { + const { getVisData } = this.plugins.metrics; if (typeof getVisData !== 'function') { throw new Error('TSVB is not available'); } - - // getBasePath returns randomized base path AND spaces path - const basePath = internalRequest.getBasePath(); - const url = `${basePath}/api/metrics/vis/data`; - + const url = this.core.http.basePath.prepend('/api/metrics/vis/data'); // For the following request we need a copy of the instnace of the internal request // but modified for our TSVB request. This will ensure all the instance methods // are available along with our overriden values - const request = Object.assign( - Object.create(Object.getPrototypeOf(internalRequest)), - internalRequest, - { - url, - method: 'POST', - payload: { - timerange, - panels: [model], - filters, + const requestCopy = Object.assign({}, request, { + url, + method: 'POST', + payload: { + timerange, + panels: [model], + filters, + }, + // NP_NOTE: [TSVB_GROUP] Huge hack to make TSVB (getVisData()) work with raw requests that + // originate from the New Platform router (and are very different to the old request object). + // Once TSVB has migrated over to NP, and can work with the new raw requests, or ideally just + // the requestContext, this can be removed. + server: { + plugins: { + elasticsearch: this.plugins.___legacy.tsvb.elasticsearch, }, - } - ); - const result = await getVisData(request); - return result as InfraTSVBResponse; + newPlatform: { + __internals: this.plugins.___legacy.tsvb.__internals, + }, + }, + getUiSettingsService: () => requestContext.core.uiSettings.client, + getSavedObjectsClient: () => requestContext.core.savedObjects.client, + }); + return getVisData(requestCopy); } } - -export function wrapRequest( - req: InternalRequest -): InfraFrameworkRequest { - const { params, payload, query } = req; - - return { - [internalInfraFrameworkRequest]: req, - params, - payload, - query, - }; -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 547e74eecb67c..ec45171baa7b0 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -15,6 +15,7 @@ import zip from 'lodash/fp/zip'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity, constant } from 'fp-ts/lib/function'; +import { RequestHandlerContext } from 'src/core/server'; import { compareTimeKeys, isTimeKey, TimeKey } from '../../../../common/time'; import { JsonObject } from '../../../../common/typed_json'; import { @@ -24,8 +25,8 @@ import { LogSummaryBucket, } from '../../domains/log_entries_domain'; import { InfraSourceConfiguration } from '../../sources'; -import { InfraFrameworkRequest, SortedSearchHit } from '../framework'; -import { InfraBackendFrameworkAdapter } from '../framework'; +import { SortedSearchHit } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; const DAY_MILLIS = 24 * 60 * 60 * 1000; const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000, Infinity].map(days => days * DAY_MILLIS); @@ -39,10 +40,10 @@ interface LogItemHit { } export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { - constructor(private readonly framework: InfraBackendFrameworkAdapter) {} + constructor(private readonly framework: KibanaFramework) {} public async getAdjacentLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -64,7 +65,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } const documentsInInterval = await this.getLogEntryDocumentsBetween( - request, + requestContext, sourceConfiguration, fields, intervalStart, @@ -82,7 +83,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } public async getContainedLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -91,7 +92,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { highlightQuery?: LogEntryQuery ): Promise { const documents = await this.getLogEntryDocumentsBetween( - request, + requestContext, sourceConfiguration, fields, start.time, @@ -106,7 +107,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } public async getContainedLogSummaryBuckets( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, start: number, end: number, @@ -165,7 +166,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { }, }; - const response = await this.framework.callWithRequest(request, 'search', query); + const response = await this.framework.callWithRequest(requestContext, 'search', query); return pipe( LogSummaryResponseRuntimeType.decode(response), @@ -179,12 +180,12 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } public async getLogItem( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, id: string, sourceConfiguration: InfraSourceConfiguration ) { const search = (searchOptions: object) => - this.framework.callWithRequest(request, 'search', searchOptions); + this.framework.callWithRequest(requestContext, 'search', searchOptions); const params = { index: sourceConfiguration.logAlias, @@ -212,7 +213,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } private async getLogEntryDocumentsBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: number, @@ -298,7 +299,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { }; const response = await this.framework.callWithRequest( - request, + requestContext, 'search', query ); diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts index adb8c811ed57d..acd7a2528bb42 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext, KibanaRequest } from 'src/core/server'; import { InfraMetric, InfraMetricData, InfraNodeType, InfraTimerangeInput, } from '../../../graphql/types'; - import { InfraSourceConfiguration } from '../../sources'; -import { InfraFrameworkRequest } from '../framework'; export interface InfraMetricsRequestOptions { nodeIds: { @@ -27,8 +26,9 @@ export interface InfraMetricsRequestOptions { export interface InfraMetricsAdapter { getMetrics( - req: InfraFrameworkRequest, - options: InfraMetricsRequestOptions + requestContext: RequestHandlerContext, + options: InfraMetricsRequestOptions, + request: KibanaRequest // NP_TODO: temporarily needed until metrics getVisData no longer needs full request ): Promise; } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index 331abd4ffb35a..db3c516841cd4 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -6,10 +6,9 @@ import { i18n } from '@kbn/i18n'; import { flatten, get } from 'lodash'; - -import Boom from 'boom'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { InfraMetric, InfraMetricData, InfraNodeType } from '../../../graphql/types'; -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { InfraMetricsAdapter, InfraMetricsRequestOptions } from './adapter_types'; import { checkValidNode } from './lib/check_valid_node'; import { metrics } from '../../../../common/inventory_models'; @@ -17,15 +16,16 @@ import { TSVBMetricModelCreator } from '../../../../common/inventory_models/type import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; export class KibanaMetricsAdapter implements InfraMetricsAdapter { - private framework: InfraBackendFrameworkAdapter; + private framework: KibanaFramework; - constructor(framework: InfraBackendFrameworkAdapter) { + constructor(framework: KibanaFramework) { this.framework = framework; } public async getMetrics( - req: InfraFrameworkRequest, - options: InfraMetricsRequestOptions + requestContext: RequestHandlerContext, + options: InfraMetricsRequestOptions, + rawRequest: KibanaRequest // NP_TODO: Temporarily needed until metrics getVisData no longer needs full request ): Promise { const fields = { [InfraNodeType.host]: options.sourceConfiguration.fields.host, @@ -35,11 +35,11 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`; const nodeField = fields[options.nodeType]; const search = (searchOptions: object) => - this.framework.callWithRequest<{}, Aggregation>(req, 'search', searchOptions); + this.framework.callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions); const validNode = await checkValidNode(search, indexPattern, nodeField, options.nodeIds.nodeId); if (!validNode) { - throw Boom.notFound( + throw new Error( i18n.translate('xpack.infra.kibanaMetrics.nodeDoesNotExistErrorMessage', { defaultMessage: '{nodeId} does not exist.', values: { @@ -50,7 +50,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { } const requests = options.metrics.map(metricId => - this.makeTSVBRequest(metricId, options, req, nodeField) + this.makeTSVBRequest(metricId, options, rawRequest, nodeField, requestContext) ); return Promise.all(requests) @@ -92,12 +92,13 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { async makeTSVBRequest( metricId: InfraMetric, options: InfraMetricsRequestOptions, - req: InfraFrameworkRequest, - nodeField: string + req: KibanaRequest, + nodeField: string, + requestContext: RequestHandlerContext ) { const createTSVBModel = get(metrics, ['tsvb', metricId]) as TSVBMetricModelCreator | undefined; if (!createTSVBModel) { - throw Boom.badRequest( + throw new Error( i18n.translate('xpack.infra.metrics.missingTSVBModelError', { defaultMessage: 'The TSVB model for {metricId} does not exist for {nodeType}', values: { @@ -121,7 +122,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { ); const calculatedInterval = await calculateMetricInterval( this.framework, - req, + requestContext, { indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, timestampField: options.sourceConfiguration.fields.timestamp, @@ -135,7 +136,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { } if (model.id_type === 'cloud' && !options.nodeIds.cloudId) { - throw Boom.badRequest( + throw new Error( i18n.translate('xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage', { defaultMessage: 'Model for {metricId} requires a cloudId, but none was given for {nodeId}.', @@ -152,6 +153,6 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { ? [{ match: { [model.map_field_to]: id } }] : [{ match: { [nodeField]: id } }]; - return this.framework.makeTSVBRequest(req, model, timerange, filters); + return this.framework.makeTSVBRequest(req, model, timerange, filters, requestContext); } } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts index e66da3f3fa6cb..635f6ff9762c5 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts @@ -4,26 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'src/core/server'; import { InfraSourceStatusAdapter } from '../../source_status'; -import { - InfraBackendFrameworkAdapter, - InfraDatabaseGetIndicesResponse, - InfraFrameworkRequest, -} from '../framework'; +import { InfraDatabaseGetIndicesResponse } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusAdapter { - constructor(private readonly framework: InfraBackendFrameworkAdapter) {} + constructor(private readonly framework: KibanaFramework) {} - public async getIndexNames(request: InfraFrameworkRequest, aliasName: string) { + public async getIndexNames(requestContext: RequestHandlerContext, aliasName: string) { const indexMaps = await Promise.all([ this.framework - .callWithRequest(request, 'indices.getAlias', { + .callWithRequest(requestContext, 'indices.getAlias', { name: aliasName, filterPath: '*.settings.index.uuid', // to keep the response size as small as possible }) .catch(withDefaultIfNotFound({})), this.framework - .callWithRequest(request, 'indices.get', { + .callWithRequest(requestContext, 'indices.get', { index: aliasName, filterPath: '*.settings.index.uuid', // to keep the response size as small as possible }) @@ -36,15 +34,15 @@ export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusA ); } - public async hasAlias(request: InfraFrameworkRequest, aliasName: string) { - return await this.framework.callWithRequest(request, 'indices.existsAlias', { + public async hasAlias(requestContext: RequestHandlerContext, aliasName: string) { + return await this.framework.callWithRequest(requestContext, 'indices.existsAlias', { name: aliasName, }); } - public async hasIndices(request: InfraFrameworkRequest, indexNames: string) { + public async hasIndices(requestContext: RequestHandlerContext, indexNames: string) { return await this.framework - .callWithRequest(request, 'search', { + .callWithRequest(requestContext, 'search', { ignore_unavailable: true, allow_no_indices: true, index: indexNames, diff --git a/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts index 215c41bcf6b7c..305841aa52d36 100644 --- a/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts @@ -3,12 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { Server } from 'hapi'; - -import { InfraKibanaConfigurationAdapter } from '../adapters/configuration/kibana_configuration_adapter'; import { FrameworkFieldsAdapter } from '../adapters/fields/framework_fields_adapter'; -import { InfraKibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaLogEntriesAdapter } from '../adapters/log_entries/kibana_log_entries_adapter'; import { KibanaMetricsAdapter } from '../adapters/metrics/kibana_metrics_adapter'; import { InfraElasticsearchSourceStatusAdapter } from '../adapters/source_status'; @@ -20,13 +16,14 @@ import { InfraLogAnalysis } from '../log_analysis'; import { InfraSnapshot } from '../snapshot'; import { InfraSourceStatus } from '../source_status'; import { InfraSources } from '../sources'; +import { InfraConfig } from '../../../../../../plugins/infra/server'; +import { CoreSetup } from '../../../../../../../src/core/server'; +import { InfraServerPluginDeps } from '../adapters/framework/adapter_types'; -export function compose(server: Server): InfraBackendLibs { - const configuration = new InfraKibanaConfigurationAdapter(server); - const framework = new InfraKibanaBackendFrameworkAdapter(server); +export function compose(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) { + const framework = new KibanaFramework(core, config, plugins); const sources = new InfraSources({ - configuration, - savedObjects: framework.getSavedObjectsService(), + config, }); const sourceStatus = new InfraSourceStatus(new InfraElasticsearchSourceStatusAdapter(framework), { sources, @@ -34,6 +31,7 @@ export function compose(server: Server): InfraBackendLibs { const snapshot = new InfraSnapshot({ sources, framework }); const logAnalysis = new InfraLogAnalysis({ framework }); + // TODO: separate these out individually and do away with "domains" as a temporary group const domainLibs: InfraDomainLibs = { fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { sources, @@ -45,7 +43,7 @@ export function compose(server: Server): InfraBackendLibs { }; const libs: InfraBackendLibs = { - configuration, + configuration: config, // NP_TODO: Do we ever use this anywhere? framework, logAnalysis, snapshot, diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts index c5a3bbeb87449..a00c76216da4c 100644 --- a/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts +++ b/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'src/core/server'; import { InfraIndexField, InfraIndexType } from '../../graphql/types'; import { FieldsAdapter } from '../adapters/fields'; -import { InfraFrameworkRequest } from '../adapters/framework'; import { InfraSources } from '../sources'; export class InfraFieldsDomain { @@ -16,16 +16,19 @@ export class InfraFieldsDomain { ) {} public async getFields( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, indexType: InfraIndexType ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const includeMetricIndices = [InfraIndexType.ANY, InfraIndexType.METRICS].includes(indexType); const includeLogIndices = [InfraIndexType.ANY, InfraIndexType.LOGS].includes(indexType); const fields = await this.adapter.getIndexFields( - request, + requestContext, `${includeMetricIndices ? configuration.metricAlias : ''},${ includeLogIndices ? configuration.logAlias : '' }`, diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 0127f80b31357..597073b1e901f 100644 --- a/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -7,6 +7,7 @@ import stringify from 'json-stable-stringify'; import { sortBy } from 'lodash'; +import { RequestHandlerContext } from 'src/core/server'; import { TimeKey } from '../../../../common/time'; import { JsonObject } from '../../../../common/typed_json'; import { @@ -16,7 +17,6 @@ import { InfraLogSummaryBucket, InfraLogSummaryHighlightBucket, } from '../../../graphql/types'; -import { InfraFrameworkRequest } from '../../adapters/framework'; import { InfraSourceConfiguration, InfraSources, @@ -40,7 +40,7 @@ export class InfraLogEntriesDomain { ) {} public async getLogEntriesAround( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, key: TimeKey, maxCountBefore: number, @@ -55,14 +55,17 @@ export class InfraLogEntriesDomain { }; } - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); const requiredFields = getRequiredFields(configuration, messageFormattingRules); const documentsBefore = await this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, key, @@ -80,7 +83,7 @@ export class InfraLogEntriesDomain { }; const documentsAfter = await this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, lastKeyBefore, @@ -101,20 +104,23 @@ export class InfraLogEntriesDomain { } public async getLogEntriesBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, startKey: TimeKey, endKey: TimeKey, filterQuery?: LogEntryQuery, highlightQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); const requiredFields = getRequiredFields(configuration, messageFormattingRules); const documents = await this.adapter.getContainedLogEntryDocuments( - request, + requestContext, configuration, requiredFields, startKey, @@ -129,7 +135,7 @@ export class InfraLogEntriesDomain { } public async getLogEntryHighlights( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, startKey: TimeKey, endKey: TimeKey, @@ -140,7 +146,10 @@ export class InfraLogEntriesDomain { }>, filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); @@ -158,7 +167,7 @@ export class InfraLogEntriesDomain { : highlightQuery; const [documentsBefore, documents, documentsAfter] = await Promise.all([ this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, startKey, @@ -168,7 +177,7 @@ export class InfraLogEntriesDomain { highlightQuery ), this.adapter.getContainedLogEntryDocuments( - request, + requestContext, configuration, requiredFields, startKey, @@ -177,7 +186,7 @@ export class InfraLogEntriesDomain { highlightQuery ), this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, endKey, @@ -203,16 +212,19 @@ export class InfraLogEntriesDomain { } public async getLogSummaryBucketsBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, start: number, end: number, bucketSize: number, filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets( - request, + requestContext, configuration, start, end, @@ -223,7 +235,7 @@ export class InfraLogEntriesDomain { } public async getLogSummaryHighlightBucketsBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, start: number, end: number, @@ -231,7 +243,10 @@ export class InfraLogEntriesDomain { highlightQueries: string[], filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); @@ -248,7 +263,7 @@ export class InfraLogEntriesDomain { } : highlightQuery; const summaryBuckets = await this.adapter.getContainedLogSummaryBuckets( - request, + requestContext, configuration, start, end, @@ -266,11 +281,11 @@ export class InfraLogEntriesDomain { } public async getLogItem( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, id: string, sourceConfiguration: InfraSourceConfiguration ): Promise { - const document = await this.adapter.getLogItem(request, id, sourceConfiguration); + const document = await this.adapter.getLogItem(requestContext, id, sourceConfiguration); const defaultFields = [ { field: '_index', value: document._index }, { field: '_id', value: document._id }, @@ -300,7 +315,7 @@ interface LogItemHit { export interface LogEntriesAdapter { getAdjacentLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -311,7 +326,7 @@ export interface LogEntriesAdapter { ): Promise; getContainedLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -321,7 +336,7 @@ export interface LogEntriesAdapter { ): Promise; getContainedLogSummaryBuckets( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, start: number, end: number, @@ -330,7 +345,7 @@ export interface LogEntriesAdapter { ): Promise; getLogItem( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, id: string, source: InfraSourceConfiguration ): Promise; diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts index 862ca8b4c823f..5d7d54a6a2e50 100644 --- a/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts +++ b/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { InfraMetricData } from '../../graphql/types'; -import { InfraFrameworkRequest } from '../adapters/framework/adapter_types'; import { InfraMetricsAdapter, InfraMetricsRequestOptions } from '../adapters/metrics/adapter_types'; export class InfraMetricsDomain { @@ -16,9 +16,10 @@ export class InfraMetricsDomain { } public async getMetrics( - req: InfraFrameworkRequest, - options: InfraMetricsRequestOptions + requestContext: RequestHandlerContext, + options: InfraMetricsRequestOptions, + rawRequest: KibanaRequest // NP_TODO: temporarily needed until metrics getVisData no longer needs full request ): Promise { - return await this.adapter.getMetrics(req, options); + return await this.adapter.getMetrics(requestContext, options, rawRequest); } } diff --git a/x-pack/legacy/plugins/infra/server/lib/infra_types.ts b/x-pack/legacy/plugins/infra/server/lib/infra_types.ts index b436bb7e4fe58..46d32885600df 100644 --- a/x-pack/legacy/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/infra_types.ts @@ -5,8 +5,6 @@ */ import { InfraSourceConfiguration } from '../../public/graphql/types'; -import { InfraConfigurationAdapter } from './adapters/configuration'; -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from './adapters/framework'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; @@ -14,6 +12,15 @@ import { InfraLogAnalysis } from './log_analysis/log_analysis'; import { InfraSnapshot } from './snapshot'; import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; +import { InfraConfig } from '../../../../../plugins/infra/server'; +import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; + +// NP_TODO: We shouldn't need this context anymore but I am +// not sure how the graphql stuff uses it, so we can't remove it yet +export interface InfraContext { + req: any; + rawReq?: any; +} export interface InfraDomainLibs { fields: InfraFieldsDomain; @@ -22,8 +29,8 @@ export interface InfraDomainLibs { } export interface InfraBackendLibs extends InfraDomainLibs { - configuration: InfraConfigurationAdapter; - framework: InfraBackendFrameworkAdapter; + configuration: InfraConfig; + framework: KibanaFramework; logAnalysis: InfraLogAnalysis; snapshot: InfraSnapshot; sources: InfraSources; @@ -40,7 +47,3 @@ export interface InfraConfiguration { default: InfraSourceConfiguration; }; } - -export interface InfraContext { - req: InfraFrameworkRequest; -} diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts index d970a142c5c23..fac49a7980f26 100644 --- a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts @@ -9,7 +9,7 @@ import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { getJobId } from '../../../common/log_analysis'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../adapters/framework'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { NoLogRateResultsIndexError } from './errors'; import { logRateModelPlotResponseRT, @@ -17,37 +17,38 @@ import { LogRateModelPlotBucket, CompositeTimestampPartitionKey, } from './queries'; +import { RequestHandlerContext, KibanaRequest } from '../../../../../../../src/core/server'; const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; export class InfraLogAnalysis { constructor( private readonly libs: { - framework: InfraBackendFrameworkAdapter; + framework: KibanaFramework; } ) {} - public getJobIds(request: InfraFrameworkRequest, sourceId: string) { + public getJobIds(request: KibanaRequest, sourceId: string) { return { logEntryRate: getJobId(this.libs.framework.getSpaceId(request), sourceId, 'log-entry-rate'), }; } public async getLogEntryRateBuckets( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, startTime: number, endTime: number, - bucketDuration: number + bucketDuration: number, + request: KibanaRequest ) { const logRateJobId = this.getJobIds(request, sourceId).logEntryRate; - let mlModelPlotBuckets: LogRateModelPlotBucket[] = []; let afterLatestBatchKey: CompositeTimestampPartitionKey | undefined; while (true) { const mlModelPlotResponse = await this.libs.framework.callWithRequest( - request, + requestContext, 'search', createLogEntryRateQuery( logRateJobId, diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts index 741293f61056e..59a4e8911a94d 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts @@ -5,6 +5,7 @@ */ import { idx } from '@kbn/elastic-idx'; +import { RequestHandlerContext } from 'src/core/server'; import { InfraSnapshotGroupbyInput, InfraSnapshotMetricInput, @@ -13,11 +14,8 @@ import { InfraNodeType, InfraSourceConfiguration, } from '../../graphql/types'; -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraDatabaseSearchResponse, -} from '../adapters/framework'; +import { InfraDatabaseSearchResponse } from '../adapters/framework'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraSources } from '../sources'; import { JsonObject } from '../../../common/typed_json'; @@ -49,20 +47,18 @@ export interface InfraSnapshotRequestOptions { } export class InfraSnapshot { - constructor( - private readonly libs: { sources: InfraSources; framework: InfraBackendFrameworkAdapter } - ) {} + constructor(private readonly libs: { sources: InfraSources; framework: KibanaFramework }) {} public async getNodes( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, options: InfraSnapshotRequestOptions ): Promise { // Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch // in order to page through the results of their respective composite aggregations. // Both chains of requests are supposed to run in parallel, and their results be merged // when they have both been completed. - const groupedNodesPromise = requestGroupedNodes(request, options, this.libs.framework); - const nodeMetricsPromise = requestNodeMetrics(request, options, this.libs.framework); + const groupedNodesPromise = requestGroupedNodes(requestContext, options, this.libs.framework); + const nodeMetricsPromise = requestNodeMetrics(requestContext, options, this.libs.framework); const groupedNodeBuckets = await groupedNodesPromise; const nodeMetricBuckets = await nodeMetricsPromise; @@ -79,9 +75,9 @@ const handleAfterKey = createAfterKeyHandler('body.aggregations.nodes.composite. ); const requestGroupedNodes = async ( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, options: InfraSnapshotRequestOptions, - framework: InfraBackendFrameworkAdapter + framework: KibanaFramework ): Promise => { const query = { allowNoIndices: true, @@ -130,13 +126,13 @@ const requestGroupedNodes = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeGroupByBucket - >(framework, request, query, bucketSelector, handleAfterKey); + >(framework, requestContext, query, bucketSelector, handleAfterKey); }; const requestNodeMetrics = async ( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, options: InfraSnapshotRequestOptions, - framework: InfraBackendFrameworkAdapter + framework: KibanaFramework ): Promise => { const index = options.metric.type === 'logRate' @@ -191,7 +187,7 @@ const requestNodeMetrics = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeMetricsBucket - >(framework, request, query, bucketSelector, handleAfterKey); + >(framework, requestContext, query, bucketSelector, handleAfterKey); }; // buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[] diff --git a/x-pack/legacy/plugins/infra/server/lib/source_status.ts b/x-pack/legacy/plugins/infra/server/lib/source_status.ts index f9f37b5aa9e5a..1f0845b6b223f 100644 --- a/x-pack/legacy/plugins/infra/server/lib/source_status.ts +++ b/x-pack/legacy/plugins/infra/server/lib/source_status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraFrameworkRequest } from './adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; import { InfraSources } from './sources'; export class InfraSourceStatus { @@ -14,58 +14,85 @@ export class InfraSourceStatus { ) {} public async getLogIndexNames( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string ): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const indexNames = await this.adapter.getIndexNames( - request, + requestContext, sourceConfiguration.configuration.logAlias ); return indexNames; } public async getMetricIndexNames( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string ): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const indexNames = await this.adapter.getIndexNames( - request, + requestContext, sourceConfiguration.configuration.metricAlias ); return indexNames; } - public async hasLogAlias(request: InfraFrameworkRequest, sourceId: string): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + public async hasLogAlias( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasAlias = await this.adapter.hasAlias( - request, + requestContext, sourceConfiguration.configuration.logAlias ); return hasAlias; } - public async hasMetricAlias(request: InfraFrameworkRequest, sourceId: string): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + public async hasMetricAlias( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasAlias = await this.adapter.hasAlias( - request, + requestContext, sourceConfiguration.configuration.metricAlias ); return hasAlias; } - public async hasLogIndices(request: InfraFrameworkRequest, sourceId: string): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + public async hasLogIndices( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasIndices = await this.adapter.hasIndices( - request, + requestContext, sourceConfiguration.configuration.logAlias ); return hasIndices; } public async hasMetricIndices( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string ): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasIndices = await this.adapter.hasIndices( - request, + requestContext, sourceConfiguration.configuration.metricAlias ); return hasIndices; @@ -73,7 +100,7 @@ export class InfraSourceStatus { } export interface InfraSourceStatusAdapter { - getIndexNames(request: InfraFrameworkRequest, aliasName: string): Promise; - hasAlias(request: InfraFrameworkRequest, aliasName: string): Promise; - hasIndices(request: InfraFrameworkRequest, indexNames: string): Promise; + getIndexNames(requestContext: RequestHandlerContext, aliasName: string): Promise; + hasAlias(requestContext: RequestHandlerContext, aliasName: string): Promise; + hasIndices(requestContext: RequestHandlerContext, indexNames: string): Promise; } diff --git a/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts index 2374a83a642df..4a83ca730ff83 100644 --- a/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts @@ -3,34 +3,31 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { InfraInmemoryConfigurationAdapter } from '../adapters/configuration/inmemory_configuration_adapter'; import { InfraSources } from './sources'; describe('the InfraSources lib', () => { describe('getSourceConfiguration method', () => { test('returns a source configuration if it exists', async () => { const sourcesLib = new InfraSources({ - configuration: createMockStaticConfiguration({}), - savedObjects: createMockSavedObjectsService({ - id: 'TEST_ID', - version: 'foo', - updated_at: '2000-01-01T00:00:00.000Z', - attributes: { - metricAlias: 'METRIC_ALIAS', - logAlias: 'LOG_ALIAS', - fields: { - container: 'CONTAINER', - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, - }, - }), + config: createMockStaticConfiguration({}), }); - const request: any = Symbol(); + const request: any = createRequestContext({ + id: 'TEST_ID', + version: 'foo', + updated_at: '2000-01-01T00:00:00.000Z', + attributes: { + metricAlias: 'METRIC_ALIAS', + logAlias: 'LOG_ALIAS', + fields: { + container: 'CONTAINER', + host: 'HOST', + pod: 'POD', + tiebreaker: 'TIEBREAKER', + timestamp: 'TIMESTAMP', + }, + }, + }); expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', @@ -52,7 +49,7 @@ describe('the InfraSources lib', () => { test('adds missing attributes from the static configuration to a source configuration', async () => { const sourcesLib = new InfraSources({ - configuration: createMockStaticConfiguration({ + config: createMockStaticConfiguration({ default: { metricAlias: 'METRIC_ALIAS', logAlias: 'LOG_ALIAS', @@ -64,19 +61,18 @@ describe('the InfraSources lib', () => { }, }, }), - savedObjects: createMockSavedObjectsService({ - id: 'TEST_ID', - version: 'foo', - updated_at: '2000-01-01T00:00:00.000Z', - attributes: { - fields: { - container: 'CONTAINER', - }, - }, - }), }); - const request: any = Symbol(); + const request: any = createRequestContext({ + id: 'TEST_ID', + version: 'foo', + updated_at: '2000-01-01T00:00:00.000Z', + attributes: { + fields: { + container: 'CONTAINER', + }, + }, + }); expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', @@ -98,16 +94,15 @@ describe('the InfraSources lib', () => { test('adds missing attributes from the default configuration to a source configuration', async () => { const sourcesLib = new InfraSources({ - configuration: createMockStaticConfiguration({}), - savedObjects: createMockSavedObjectsService({ - id: 'TEST_ID', - version: 'foo', - updated_at: '2000-01-01T00:00:00.000Z', - attributes: {}, - }), + config: createMockStaticConfiguration({}), }); - const request: any = Symbol(); + const request: any = createRequestContext({ + id: 'TEST_ID', + version: 'foo', + updated_at: '2000-01-01T00:00:00.000Z', + attributes: {}, + }); expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', @@ -129,29 +124,30 @@ describe('the InfraSources lib', () => { }); }); -const createMockStaticConfiguration = (sources: any) => - new InfraInmemoryConfigurationAdapter({ - enabled: true, - query: { - partitionSize: 1, - partitionFactor: 1, - }, - sources, - }); - -const createMockSavedObjectsService = (savedObject?: any) => ({ - getScopedSavedObjectsClient() { - return { - async get() { - return savedObject; - }, - } as any; +const createMockStaticConfiguration = (sources: any) => ({ + enabled: true, + query: { + partitionSize: 1, + partitionFactor: 1, }, - SavedObjectsClient: { - errors: { - isNotFoundError() { - return typeof savedObject === 'undefined'; + sources, +}); + +const createRequestContext = (savedObject?: any) => { + return { + core: { + savedObjects: { + client: { + async get() { + return savedObject; + }, + errors: { + isNotFoundError() { + return typeof savedObject === 'undefined'; + }, + }, + }, }, }, - }, -}); + }; +}; diff --git a/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts b/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts index 951556a0fe642..2b38d81e4a8d5 100644 --- a/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts @@ -6,14 +6,10 @@ import * as runtimeTypes from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; -import { Legacy } from 'kibana'; - import { identity, constant } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; -import { Pick3 } from '../../../common/utility_types'; -import { InfraConfigurationAdapter } from '../adapters/configuration'; -import { InfraFrameworkRequest, internalInfraFrameworkRequest } from '../adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; import { NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings'; @@ -25,19 +21,21 @@ import { SourceConfigurationSavedObjectRuntimeType, StaticSourceConfigurationRuntimeType, } from './types'; +import { InfraConfig } from '../../../../../../plugins/infra/server'; + +interface Libs { + config: InfraConfig; +} export class InfraSources { private internalSourceConfigurations: Map = new Map(); + private readonly libs: Libs; - constructor( - private readonly libs: { - configuration: InfraConfigurationAdapter; - savedObjects: Pick & - Pick3; - } - ) {} + constructor(libs: Libs) { + this.libs = libs; + } - public async getSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) { + public async getSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId) @@ -53,7 +51,7 @@ export class InfraSources { })) .catch(err => err instanceof NotFoundError - ? this.getSavedSourceConfiguration(request, sourceId).then(result => ({ + ? this.getSavedSourceConfiguration(requestContext, sourceId).then(result => ({ ...result, configuration: mergeSourceConfiguration( staticDefaultSourceConfiguration, @@ -63,7 +61,7 @@ export class InfraSources { : Promise.reject(err) ) .catch(err => - this.libs.savedObjects.SavedObjectsClient.errors.isNotFoundError(err) + requestContext.core.savedObjects.client.errors.isNotFoundError(err) ? Promise.resolve({ id: sourceId, version: undefined, @@ -77,10 +75,10 @@ export class InfraSources { return savedSourceConfiguration; } - public async getAllSourceConfigurations(request: InfraFrameworkRequest) { + public async getAllSourceConfigurations(requestContext: RequestHandlerContext) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(request); + const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(requestContext); return savedSourceConfigurations.map(savedSourceConfiguration => ({ ...savedSourceConfiguration, @@ -92,7 +90,7 @@ export class InfraSources { } public async createSourceConfiguration( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, source: InfraSavedSourceConfiguration ) { @@ -104,13 +102,11 @@ export class InfraSources { ); const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await this.libs.savedObjects - .getScopedSavedObjectsClient(request[internalInfraFrameworkRequest]) - .create( - infraSourceConfigurationSavedObjectType, - pickSavedSourceConfiguration(newSourceConfiguration) as any, - { id: sourceId } - ) + await requestContext.core.savedObjects.client.create( + infraSourceConfigurationSavedObjectType, + pickSavedSourceConfiguration(newSourceConfiguration) as any, + { id: sourceId } + ) ); return { @@ -122,20 +118,21 @@ export class InfraSources { }; } - public async deleteSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) { - await this.libs.savedObjects - .getScopedSavedObjectsClient(request[internalInfraFrameworkRequest]) - .delete(infraSourceConfigurationSavedObjectType, sourceId); + public async deleteSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { + await requestContext.core.savedObjects.client.delete( + infraSourceConfigurationSavedObjectType, + sourceId + ); } public async updateSourceConfiguration( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, sourceProperties: InfraSavedSourceConfiguration ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const { configuration, version } = await this.getSourceConfiguration(request, sourceId); + const { configuration, version } = await this.getSourceConfiguration(requestContext, sourceId); const updatedSourceConfigurationAttributes = mergeSourceConfiguration( configuration, @@ -143,16 +140,14 @@ export class InfraSources { ); const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await this.libs.savedObjects - .getScopedSavedObjectsClient(request[internalInfraFrameworkRequest]) - .update( - infraSourceConfigurationSavedObjectType, - sourceId, - pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, - { - version, - } - ) + await requestContext.core.savedObjects.client.update( + infraSourceConfigurationSavedObjectType, + sourceId, + pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, + { + version, + } + ) ); return { @@ -184,7 +179,6 @@ export class InfraSources { } private async getStaticDefaultSourceConfiguration() { - const staticConfiguration = await this.libs.configuration.get(); const staticSourceConfiguration = pipe( runtimeTypes .type({ @@ -192,7 +186,7 @@ export class InfraSources { default: StaticSourceConfigurationRuntimeType, }), }) - .decode(staticConfiguration), + .decode(this.libs.config), map(({ sources: { default: defaultConfiguration } }) => defaultConfiguration), fold(constant({}), identity) ); @@ -200,12 +194,11 @@ export class InfraSources { return mergeSourceConfiguration(defaultSourceConfiguration, staticSourceConfiguration); } - private async getSavedSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) { - const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient( - request[internalInfraFrameworkRequest] - ); - - const savedObject = await savedObjectsClient.get( + private async getSavedSourceConfiguration( + requestContext: RequestHandlerContext, + sourceId: string + ) { + const savedObject = await requestContext.core.savedObjects.client.get( infraSourceConfigurationSavedObjectType, sourceId ); @@ -213,12 +206,8 @@ export class InfraSources { return convertSavedObjectToSavedSourceConfiguration(savedObject); } - private async getAllSavedSourceConfigurations(request: InfraFrameworkRequest) { - const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient( - request[internalInfraFrameworkRequest] - ); - - const savedObjects = await savedObjectsClient.find({ + private async getAllSavedSourceConfigurations(requestContext: RequestHandlerContext) { + const savedObjects = await requestContext.core.savedObjects.client.find({ type: infraSourceConfigurationSavedObjectType, }); diff --git a/x-pack/legacy/plugins/infra/server/new_platform_index.ts b/x-pack/legacy/plugins/infra/server/new_platform_index.ts new file mode 100644 index 0000000000000..6b759ecfe9fde --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/new_platform_index.ts @@ -0,0 +1,16 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { InfraServerPlugin } from './new_platform_plugin'; +import { config, InfraConfig } from '../../../../plugins/infra/server'; +import { InfraServerPluginDeps } from './lib/adapters/framework'; + +export { config, InfraConfig, InfraServerPluginDeps }; + +export function plugin(context: PluginInitializerContext) { + return new InfraServerPlugin(context); +} diff --git a/x-pack/legacy/plugins/infra/server/new_platform_plugin.ts b/x-pack/legacy/plugins/infra/server/new_platform_plugin.ts new file mode 100644 index 0000000000000..462a07574b2dd --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/new_platform_plugin.ts @@ -0,0 +1,107 @@ +/* + * 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 { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { Server } from 'hapi'; +import { InfraConfig } from '../../../../plugins/infra/server'; +import { initInfraServer } from './infra_server'; +import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types'; +import { FrameworkFieldsAdapter } from './lib/adapters/fields/framework_fields_adapter'; +import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter'; +import { InfraKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter'; +import { KibanaMetricsAdapter } from './lib/adapters/metrics/kibana_metrics_adapter'; +import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_status'; +import { InfraFieldsDomain } from './lib/domains/fields_domain'; +import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain'; +import { InfraMetricsDomain } from './lib/domains/metrics_domain'; +import { InfraLogAnalysis } from './lib/log_analysis'; +import { InfraSnapshot } from './lib/snapshot'; +import { InfraSourceStatus } from './lib/source_status'; +import { InfraSources } from './lib/sources'; +import { InfraServerPluginDeps } from './lib/adapters/framework'; +import { METRICS_FEATURE, LOGS_FEATURE } from './features'; +import { UsageCollector } from './usage/usage_collector'; + +export interface KbnServer extends Server { + usage: any; +} + +const DEFAULT_CONFIG: InfraConfig = { + enabled: true, + query: { + partitionSize: 75, + partitionFactor: 1.2, + }, +}; + +export class InfraServerPlugin { + public config: InfraConfig = DEFAULT_CONFIG; + public libs: InfraBackendLibs | undefined; + + constructor(context: PluginInitializerContext) { + const config$ = context.config.create(); + config$.subscribe(configValue => { + this.config = { + ...DEFAULT_CONFIG, + enabled: configValue.enabled, + query: { + ...DEFAULT_CONFIG.query, + ...configValue.query, + }, + }; + }); + } + + getLibs() { + if (!this.libs) { + throw new Error('libs not set up yet'); + } + return this.libs; + } + + setup(core: CoreSetup, plugins: InfraServerPluginDeps) { + const framework = new KibanaFramework(core, this.config, plugins); + const sources = new InfraSources({ + config: this.config, + }); + const sourceStatus = new InfraSourceStatus( + new InfraElasticsearchSourceStatusAdapter(framework), + { + sources, + } + ); + const snapshot = new InfraSnapshot({ sources, framework }); + const logAnalysis = new InfraLogAnalysis({ framework }); + + // TODO: separate these out individually and do away with "domains" as a temporary group + const domainLibs: InfraDomainLibs = { + fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { + sources, + }), + logEntries: new InfraLogEntriesDomain(new InfraKibanaLogEntriesAdapter(framework), { + sources, + }), + metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), + }; + + this.libs = { + configuration: this.config, + framework, + logAnalysis, + snapshot, + sources, + sourceStatus, + ...domainLibs, + }; + + plugins.features.registerFeature(METRICS_FEATURE); + plugins.features.registerFeature(LOGS_FEATURE); + + initInfraServer(this.libs); + + // Telemetry + UsageCollector.registerUsageCollector(plugins.usageCollection); + } +} diff --git a/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts b/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts index 16837298f0704..5ad79b3d17a13 100644 --- a/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts +++ b/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts @@ -3,18 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; -import { boomify, notFound } from 'boom'; import { first } from 'lodash'; +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../lib/infra_types'; -import { InfraWrappableRequest } from '../lib/adapters/framework'; - -interface IpToHostRequest { - ip: string; - index_pattern: string; -} - -type IpToHostWrappedRequest = InfraWrappableRequest; export interface IpToHostResponse { host: string; @@ -28,40 +19,47 @@ interface HostDoc { }; } -const ipToHostSchema = Joi.object({ - ip: Joi.string().required(), - index_pattern: Joi.string().required(), +const ipToHostSchema = schema.object({ + ip: schema.string(), + index_pattern: schema.string(), }); export const initIpToHostName = ({ framework }: InfraBackendLibs) => { const { callWithRequest } = framework; - framework.registerRoute>({ - method: 'POST', - path: '/api/infra/ip_to_host', - options: { - validate: { payload: ipToHostSchema }, + framework.registerRoute( + { + method: 'post', + path: '/api/infra/ip_to_host', + validate: { + body: ipToHostSchema, + }, }, - handler: async req => { + async (requestContext, { body }, response) => { try { const params = { - index: req.payload.index_pattern, + index: body.index_pattern, body: { size: 1, query: { - match: { 'host.ip': req.payload.ip }, + match: { 'host.ip': body.ip }, }, _source: ['host.name'], }, }; - const response = await callWithRequest(req, 'search', params); - if (response.hits.total.value === 0) { - throw notFound('Host with matching IP address not found.'); + const { hits } = await callWithRequest(requestContext, 'search', params); + if (hits.total.value === 0) { + return response.notFound({ + body: { message: 'Host with matching IP address not found.' }, + }); } - const hostDoc = first(response.hits.hits); - return { host: hostDoc._source.host.name }; - } catch (e) { - throw boomify(e); + const hostDoc = first(hits.hits); + return response.ok({ body: { host: hostDoc._source.host.name } }); + } catch ({ statusCode = 500, message = 'Unknown error occurred' }) { + return response.customError({ + statusCode, + body: { message }, + }); } - }, - }); + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts index 0a369adb7ca29..1f64da1859b5f 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts @@ -8,7 +8,7 @@ import Boom from 'boom'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; - +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { LOG_ANALYSIS_VALIDATION_INDICES_PATH, @@ -20,64 +20,75 @@ import { import { throwErrors } from '../../../../common/runtime_types'; const partitionField = 'event.dataset'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) => { - framework.registerRoute({ - method: 'POST', - path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, - handler: async (req, res) => { - const payload = pipe( - validationIndicesRequestPayloadRT.decode(req.payload), - fold(throwErrors(Boom.badRequest), identity) - ); - - const { timestampField, indices } = payload.data; - const errors: ValidationIndicesError[] = []; + framework.registerRoute( + { + method: 'post', + path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + validate: { body: escapeHatch }, + }, + async (requestContext, request, response) => { + try { + const payload = pipe( + validationIndicesRequestPayloadRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - // Query each pattern individually, to map correctly the errors - await Promise.all( - indices.map(async index => { - const fieldCaps = await framework.callWithRequest(req, 'fieldCaps', { - index, - fields: `${timestampField},${partitionField}`, - }); + const { timestampField, indices } = payload.data; + const errors: ValidationIndicesError[] = []; - if (fieldCaps.indices.length === 0) { - errors.push({ - error: 'INDEX_NOT_FOUND', + // Query each pattern individually, to map correctly the errors + await Promise.all( + indices.map(async index => { + const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', { index, + fields: `${timestampField},${partitionField}`, }); - return; - } - ([ - [timestampField, 'date'], - [partitionField, 'keyword'], - ] as const).forEach(([field, fieldType]) => { - const fieldMetadata = fieldCaps.fields[field]; - - if (fieldMetadata === undefined) { + if (fieldCaps.indices.length === 0) { errors.push({ - error: 'FIELD_NOT_FOUND', + error: 'INDEX_NOT_FOUND', index, - field, }); - } else { - const fieldTypes = Object.keys(fieldMetadata); + return; + } + + ([ + [timestampField, 'date'], + [partitionField, 'keyword'], + ] as const).forEach(([field, fieldType]) => { + const fieldMetadata = fieldCaps.fields[field]; - if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + if (fieldMetadata === undefined) { errors.push({ - error: `FIELD_NOT_VALID`, + error: 'FIELD_NOT_FOUND', index, field, }); - } - } - }); - }) - ); + } else { + const fieldTypes = Object.keys(fieldMetadata); - return res.response(validationIndicesResponsePayloadRT.encode({ data: { errors } })); - }, - }); + if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + errors.push({ + error: `FIELD_NOT_VALID`, + index, + field, + }); + } + } + }); + }) + ); + return response.ok({ + body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts index fc06ea48f4353..973080c880e6d 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts @@ -9,6 +9,7 @@ import Boom from 'boom'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, @@ -19,46 +20,58 @@ import { import { throwErrors } from '../../../../common/runtime_types'; import { NoLogRateResultsIndexError } from '../../../lib/log_analysis'; +const anyObject = schema.object({}, { allowUnknowns: true }); + export const initLogAnalysisGetLogEntryRateRoute = ({ framework, logAnalysis, }: InfraBackendLibs) => { - framework.registerRoute({ - method: 'POST', - path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, - handler: async (req, res) => { + framework.registerRoute( + { + method: 'post', + path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, + validate: { + // short-circuit forced @kbn/config-schema validation so we can do io-ts validation + body: anyObject, + }, + }, + async (requestContext, request, response) => { const payload = pipe( - getLogEntryRateRequestPayloadRT.decode(req.payload), + getLogEntryRateRequestPayloadRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const logEntryRateBuckets = await logAnalysis - .getLogEntryRateBuckets( - req, + try { + const logEntryRateBuckets = await logAnalysis.getLogEntryRateBuckets( + requestContext, payload.data.sourceId, payload.data.timeRange.startTime, payload.data.timeRange.endTime, - payload.data.bucketDuration - ) - .catch(err => { - if (err instanceof NoLogRateResultsIndexError) { - throw Boom.boomify(err, { statusCode: 404 }); - } + payload.data.bucketDuration, + request + ); - throw Boom.boomify(err, { statusCode: ('statusCode' in err && err.statusCode) || 500 }); + return response.ok({ + body: getLogEntryRateSuccessReponsePayloadRT.encode({ + data: { + bucketDuration: payload.data.bucketDuration, + histogramBuckets: logEntryRateBuckets, + totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets), + }, + }), }); - - return res.response( - getLogEntryRateSuccessReponsePayloadRT.encode({ - data: { - bucketDuration: payload.data.bucketDuration, - histogramBuckets: logEntryRateBuckets, - totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets), - }, - }) - ); - }, - }); + } catch (e) { + const { statusCode = 500, message = 'Unknown error occurred' } = e; + if (e instanceof NoLogRateResultsIndexError) { + return response.notFound({ body: { message } }); + } + return response.customError({ + statusCode, + body: { message }, + }); + } + } + ); }; const getTotalNumberOfLogEntries = ( diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts index 8cdb121aebf1e..a1f6311a103eb 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom, { boomify } from 'boom'; +import { schema } from '@kbn/config-schema'; +import Boom from 'boom'; import { get } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { - InfraMetadata, - InfraMetadataWrappedRequest, InfraMetadataFeature, InfraMetadataRequestRT, InfraMetadataRT, @@ -24,23 +23,33 @@ import { getCloudMetricsMetadata } from './lib/get_cloud_metric_metadata'; import { getNodeInfo } from './lib/get_node_info'; import { throwErrors } from '../../../common/runtime_types'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); + export const initMetadataRoute = (libs: InfraBackendLibs) => { const { framework } = libs; - framework.registerRoute>({ - method: 'POST', - path: '/api/infra/metadata', - handler: async req => { + framework.registerRoute( + { + method: 'post', + path: '/api/infra/metadata', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { try { const { nodeId, nodeType, sourceId } = pipe( - InfraMetadataRequestRT.decode(req.payload), + InfraMetadataRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const { configuration } = await libs.sources.getSourceConfiguration(req, sourceId); + const { configuration } = await libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const metricsMetadata = await getMetricMetadata( framework, - req, + requestContext, configuration, nodeId, nodeType @@ -49,35 +58,35 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { nameToFeature('metrics') ); - const info = await getNodeInfo(framework, req, configuration, nodeId, nodeType); + const info = await getNodeInfo(framework, requestContext, configuration, nodeId, nodeType); const cloudInstanceId = get(info, 'cloud.instance.id'); const cloudMetricsMetadata = cloudInstanceId - ? await getCloudMetricsMetadata(framework, req, configuration, cloudInstanceId) + ? await getCloudMetricsMetadata(framework, requestContext, configuration, cloudInstanceId) : { buckets: [] }; const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( nameToFeature('metrics') ); - - const hasAPM = await hasAPMData(framework, req, configuration, nodeId, nodeType); + const hasAPM = await hasAPMData(framework, requestContext, configuration, nodeId, nodeType); const apmMetricFeatures = hasAPM ? [{ name: 'apm.transaction', source: 'apm' }] : []; const id = metricsMetadata.id; const name = metricsMetadata.name || id; - return pipe( - InfraMetadataRT.decode({ + return response.ok({ + body: InfraMetadataRT.encode({ id, name, features: [...metricFeatures, ...cloudMetricsFeatures, ...apmMetricFeatures], info, }), - fold(throwErrors(Boom.badImplementation), identity) - ); + }); } catch (error) { - throw boomify(error); + return response.internalError({ + body: error.message, + }); } - }, - }); + } + ); }; const nameToFeature = (source: string) => (name: string): InfraMetadataFeature => ({ diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts index 58b3beab42886..75ca3ae3caee2 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'src/core/server'; import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, InfraMetadataAggregationBucket, InfraMetadataAggregationResponse, } from '../../../lib/adapters/framework'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; @@ -18,8 +18,8 @@ export interface InfraCloudMetricsAdapterResponse { } export const getCloudMetricsMetadata = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, instanceId: string ): Promise => { @@ -51,7 +51,7 @@ export const getCloudMetricsMetadata = async ( { metrics?: InfraMetadataAggregationResponse; } - >(req, 'search', metricQuery); + >(requestContext, 'search', metricQuery); const buckets = response.aggregations && response.aggregations.metrics diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts index 812bc27fffc8a..3bd22062c26a0 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts @@ -5,12 +5,12 @@ */ import { get } from 'lodash'; +import { RequestHandlerContext } from 'src/core/server'; import { - InfraFrameworkRequest, InfraMetadataAggregationBucket, - InfraBackendFrameworkAdapter, InfraMetadataAggregationResponse, } from '../../../lib/adapters/framework'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { getIdFieldName } from './get_id_field_name'; import { NAME_FIELDS } from '../../../lib/constants'; @@ -22,8 +22,8 @@ export interface InfraMetricsAdapterResponse { } export const getMetricMetadata = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' @@ -69,7 +69,7 @@ export const getMetricMetadata = async ( metrics?: InfraMetadataAggregationResponse; nodeName?: InfraMetadataAggregationResponse; } - >(req, 'search', metricQuery); + >(requestContext, 'search', metricQuery); const buckets = response.aggregations && response.aggregations.metrics diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts index 5af25515a42ed..1567b6d1bd1ec 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -5,10 +5,8 @@ */ import { first } from 'lodash'; -import { - InfraFrameworkRequest, - InfraBackendFrameworkAdapter, -} from '../../../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { InfraNodeType } from '../../../graphql/types'; import { InfraMetadataInfo } from '../../../../common/http_api/metadata_api'; @@ -17,8 +15,8 @@ import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; import { getIdFieldName } from './get_id_field_name'; export const getNodeInfo = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' @@ -31,7 +29,7 @@ export const getNodeInfo = async ( if (nodeType === InfraNodeType.pod) { const kubernetesNodeName = await getPodNodeName( framework, - req, + requestContext, sourceConfiguration, nodeId, nodeType @@ -39,7 +37,7 @@ export const getNodeInfo = async ( if (kubernetesNodeName) { return getNodeInfo( framework, - req, + requestContext, sourceConfiguration, kubernetesNodeName, InfraNodeType.host @@ -64,7 +62,7 @@ export const getNodeInfo = async ( }, }; const response = await framework.callWithRequest<{ _source: InfraMetadataInfo }, {}>( - req, + requestContext, 'search', params ); diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts index 893707a4660ee..47ffc7f83b6bc 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts @@ -5,16 +5,14 @@ */ import { first, get } from 'lodash'; -import { - InfraFrameworkRequest, - InfraBackendFrameworkAdapter, -} from '../../../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { getIdFieldName } from './get_id_field_name'; export const getPodNodeName = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' @@ -40,7 +38,7 @@ export const getPodNodeName = async ( const response = await framework.callWithRequest< { _source: { kubernetes: { node: { name: string } } } }, {} - >(req, 'search', params); + >(requestContext, 'search', params); const firstHit = first(response.hits.hits); if (firstHit) { return get(firstHit, '_source.kubernetes.node.name'); diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts index 3193cf83978b0..ab242804173c0 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts @@ -4,22 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - InfraFrameworkRequest, - InfraBackendFrameworkAdapter, -} from '../../../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; + +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { getIdFieldName } from './get_id_field_name'; export const hasAPMData = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' ) => { - const config = framework.config(req); - const apmIndex = config.get('apm_oss.transactionIndices') || 'apm-*'; + const apmIndices = await framework.plugins.apm.getApmIndices( + requestContext.core.savedObjects.client + ); + const apmIndex = apmIndices['apm_oss.transactionIndices'] || 'apm-*'; + // There is a bug in APM ECS data where host.name is not set. // This will fixed with: https://github.com/elastic/apm-server/issues/2502 const nodeFieldName = @@ -48,6 +50,6 @@ export const hasAPMData = async ( }, }, }; - const response = await framework.callWithRequest<{}, {}>(req, 'search', params); + const response = await framework.callWithRequest<{}, {}>(requestContext, 'search', params); return response.hits.total.value !== 0; }; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts index 6b724f6ac60fd..0c69034c66940 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts @@ -4,42 +4,50 @@ * you may not use this file except in compliance with the Elastic License. */ -import { boomify } from 'boom'; +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../lib/infra_types'; import { getGroupings } from './lib/get_groupings'; import { populateSeriesWithTSVBData } from './lib/populate_series_with_tsvb_data'; -import { metricsExplorerSchema } from './schema'; -import { MetricsExplorerResponse, MetricsExplorerWrappedRequest } from './types'; +import { MetricsExplorerRequestBody } from './types'; +// import { metricsExplorerSchema } from './schema'; +// import { MetricsExplorerResponse, MetricsExplorerRequestBody } from './types'; + +// NP_TODO: need to replace all of this with real types or io-ts or something? +const escapeHatch = schema.object({}, { allowUnknowns: true }); export const initMetricExplorerRoute = (libs: InfraBackendLibs) => { const { framework } = libs; const { callWithRequest } = framework; - framework.registerRoute>({ - method: 'POST', - path: '/api/infra/metrics_explorer', - options: { + framework.registerRoute( + { + method: 'post', + path: '/api/infra/metrics_explorer', validate: { - payload: metricsExplorerSchema, + body: escapeHatch, }, }, - handler: async req => { + async (requestContext, request, response) => { try { const search = (searchOptions: object) => - callWithRequest<{}, Aggregation>(req, 'search', searchOptions); - const options = req.payload; + callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions); + const options = request.body as MetricsExplorerRequestBody; // Need to remove this casting and swap in config-schema demands :( // First we get the groupings from a composite aggregation - const response = await getGroupings(search, options); + const groupings = await getGroupings(search, options); // Then we take the results and fill in the data from TSVB with the // user's custom metrics const seriesWithMetrics = await Promise.all( - response.series.map(populateSeriesWithTSVBData(req, options, framework)) + groupings.series.map( + populateSeriesWithTSVBData(request, options, framework, requestContext) + ) ); - return { ...response, series: seriesWithMetrics }; + return response.ok({ body: { ...groupings, series: seriesWithMetrics } }); } catch (error) { - throw boomify(error); + return response.internalError({ + body: error.message, + }); } - }, - }); + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts index 6b7f85f7e5952..64b9fba0e7aa2 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts @@ -5,10 +5,10 @@ */ import { InfraMetricModelMetricType } from '../../../lib/adapters/metrics'; -import { MetricsExplorerAggregation, MetricsExplorerRequest } from '../types'; +import { MetricsExplorerAggregation, MetricsExplorerRequestBody } from '../types'; import { InfraMetric } from '../../../graphql/types'; import { TSVBMetricModel } from '../../../../common/inventory_models/types'; -export const createMetricModel = (options: MetricsExplorerRequest): TSVBMetricModel => { +export const createMetricModel = (options: MetricsExplorerRequestBody): TSVBMetricModel => { return { id: InfraMetric.custom, requires: [], diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts index 994de72f8029a..7111d3e7f8ca4 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts @@ -6,7 +6,7 @@ import { isObject, set } from 'lodash'; import { InfraDatabaseSearchResponse } from '../../../lib/adapters/framework'; -import { MetricsExplorerRequest, MetricsExplorerResponse } from '../types'; +import { MetricsExplorerRequestBody, MetricsExplorerResponse } from '../types'; interface GroupingAggregation { groupingsCount: { @@ -27,7 +27,7 @@ const EMPTY_RESPONSE = { export const getGroupings = async ( search: (options: object) => Promise>, - options: MetricsExplorerRequest + options: MetricsExplorerRequestBody ): Promise => { if (!options.groupBy) { return EMPTY_RESPONSE; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts index 80ccad9567a0f..1a0edb1053730 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts @@ -5,25 +5,23 @@ */ import { union } from 'lodash'; -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, -} from '../../../lib/adapters/framework'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { MetricsExplorerColumnType, - MetricsExplorerRequest, MetricsExplorerRow, MetricsExplorerSeries, - MetricsExplorerWrappedRequest, + MetricsExplorerRequestBody, } from '../types'; import { createMetricModel } from './create_metrics_model'; import { JsonObject } from '../../../../common/typed_json'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; export const populateSeriesWithTSVBData = ( - req: InfraFrameworkRequest, - options: MetricsExplorerRequest, - framework: InfraBackendFrameworkAdapter + request: KibanaRequest, + options: MetricsExplorerRequestBody, + framework: KibanaFramework, + requestContext: RequestHandlerContext ) => async (series: MetricsExplorerSeries) => { // IF there are no metrics selected then we should return an empty result. if (options.metrics.length === 0) { @@ -57,7 +55,7 @@ export const populateSeriesWithTSVBData = ( const model = createMetricModel(options); const calculatedInterval = await calculateMetricInterval( framework, - req, + requestContext, { indexPattern: options.indexPattern, timestampField: options.timerange.field, @@ -78,7 +76,13 @@ export const populateSeriesWithTSVBData = ( } // Get TSVB results using the model, timerange and filters - const tsvbResults = await framework.makeTSVBRequest(req, model, timerange, filters); + const tsvbResults = await framework.makeTSVBRequest( + request, + model, + timerange, + filters, + requestContext + ); // If there is no data `custom` will not exist. if (!tsvbResults.custom) { diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts index b29c41fcbff18..a43e3adbdd184 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraWrappableRequest } from '../../lib/adapters/framework'; - export interface InfraTimerange { field: string; from: number; @@ -27,7 +25,7 @@ export interface MetricsExplorerMetric { field?: string | undefined; } -export interface MetricsExplorerRequest { +export interface MetricsExplorerRequestBody { timerange: InfraTimerange; indexPattern: string; metrics: MetricsExplorerMetric[]; @@ -37,8 +35,6 @@ export interface MetricsExplorerRequest { filterQuery?: string; } -export type MetricsExplorerWrappedRequest = InfraWrappableRequest; - export interface MetricsExplorerPageInfo { total: number; afterKey?: string | null; diff --git a/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts b/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts index a4bc84433a4c1..a9419cd27e684 100644 --- a/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from 'boom'; -import { boomify } from 'boom'; +import { schema } from '@kbn/config-schema'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; @@ -13,27 +13,34 @@ import { UsageCollector } from '../../usage/usage_collector'; import { InfraMetricsRequestOptions } from '../../lib/adapters/metrics'; import { InfraNodeType, InfraMetric } from '../../graphql/types'; import { - NodeDetailsWrappedRequest, NodeDetailsRequestRT, - NodeDetailsMetricDataResponse, + NodeDetailsMetricDataResponseRT, } from '../../../common/http_api/node_details_api'; import { throwErrors } from '../../../common/runtime_types'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); + export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { const { framework } = libs; - framework.registerRoute>({ - method: 'POST', - path: '/api/metrics/node_details', - handler: async req => { - const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( - NodeDetailsRequestRT.decode(req.payload), - fold(throwErrors(Boom.badRequest), identity) - ); + framework.registerRoute( + { + method: 'post', + path: '/api/metrics/node_details', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { try { - const source = await libs.sources.getSourceConfiguration(req, sourceId); + const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( + NodeDetailsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); UsageCollector.countNode(nodeType); + const options: InfraMetricsRequestOptions = { nodeIds: { nodeId, @@ -44,13 +51,16 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { metrics: metrics as InfraMetric[], timerange, }; - - return { - metrics: await libs.metrics.getMetrics(req, options), - }; - } catch (e) { - throw boomify(e); + return response.ok({ + body: NodeDetailsMetricDataResponseRT.encode({ + metrics: await libs.metrics.getMetrics(requestContext, options, request), + }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); } - }, - }); + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts b/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts index 61d2fccf00101..013a261d24831 100644 --- a/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from 'boom'; +import { schema } from '@kbn/config-schema'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; @@ -12,37 +13,50 @@ import { InfraSnapshotRequestOptions } from '../../lib/snapshot'; import { UsageCollector } from '../../usage/usage_collector'; import { parseFilterQuery } from '../../utils/serialized_query'; import { InfraNodeType, InfraSnapshotMetricInput } from '../../../public/graphql/types'; -import { - SnapshotRequestRT, - SnapshotWrappedRequest, - SnapshotNodeResponse, -} from '../../../common/http_api/snapshot_api'; +import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; import { throwErrors } from '../../../common/runtime_types'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); + export const initSnapshotRoute = (libs: InfraBackendLibs) => { const { framework } = libs; - framework.registerRoute>({ - method: 'POST', - path: '/api/metrics/snapshot', - handler: async req => { - const { filterQuery, nodeType, groupBy, sourceId, metric, timerange } = pipe( - SnapshotRequestRT.decode(req.payload), - fold(throwErrors(Boom.badRequest), identity) - ); - const source = await libs.sources.getSourceConfiguration(req, sourceId); - UsageCollector.countNode(nodeType); - const options: InfraSnapshotRequestOptions = { - filterQuery: parseFilterQuery(filterQuery), - // TODO: Use common infra metric and replace graphql type - nodeType: nodeType as InfraNodeType, - groupBy, - sourceConfiguration: source.configuration, - // TODO: Use common infra metric and replace graphql type - metric: metric as InfraSnapshotMetricInput, - timerange, - }; - return { nodes: await libs.snapshot.getNodes(req, options) }; + framework.registerRoute( + { + method: 'post', + path: '/api/metrics/snapshot', + validate: { + body: escapeHatch, + }, }, - }); + async (requestContext, request, response) => { + try { + const { filterQuery, nodeType, groupBy, sourceId, metric, timerange } = pipe( + SnapshotRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + UsageCollector.countNode(nodeType); + const options: InfraSnapshotRequestOptions = { + filterQuery: parseFilterQuery(filterQuery), + // TODO: Use common infra metric and replace graphql type + nodeType: nodeType as InfraNodeType, + groupBy, + sourceConfiguration: source.configuration, + // TODO: Use common infra metric and replace graphql type + metric: metric as InfraSnapshotMetricInput, + timerange, + }; + return response.ok({ + body: SnapshotNodeResponseRT.encode({ + nodes: await libs.snapshot.getNodes(requestContext, options), + }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts index 7696abd2ac250..5eb5d424cdd73 100644 --- a/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; interface Options { indexPattern: string; @@ -20,8 +21,8 @@ interface Options { * This is useful for visualizing metric modules like s3 that only send metrics once per day. */ export const calculateMetricInterval = async ( - framework: InfraBackendFrameworkAdapter, - request: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, options: Options, modules: string[] ) => { @@ -64,7 +65,11 @@ export const calculateMetricInterval = async ( }, }; - const resp = await framework.callWithRequest<{}, PeriodAggregationData>(request, 'search', query); + const resp = await framework.callWithRequest<{}, PeriodAggregationData>( + requestContext, + 'search', + query + ); // if ES doesn't return an aggregations key, something went seriously wrong. if (!resp.aggregations) { diff --git a/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts b/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts index a5729b6004dcf..c7ff1b077f685 100644 --- a/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts +++ b/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts @@ -4,25 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraDatabaseSearchResponse, -} from '../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; +import { InfraDatabaseSearchResponse } from '../lib/adapters/framework'; export const getAllCompositeData = async < Aggregation = undefined, Bucket = {}, Options extends object = {} >( - framework: InfraBackendFrameworkAdapter, - request: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, options: Options, bucketSelector: (response: InfraDatabaseSearchResponse<{}, Aggregation>) => Bucket[], onAfterKey: (options: Options, response: InfraDatabaseSearchResponse<{}, Aggregation>) => Options, previousBuckets: Bucket[] = [] ): Promise => { - const response = await framework.callWithRequest<{}, Aggregation>(request, 'search', options); + const response = await framework.callWithRequest<{}, Aggregation>( + requestContext, + 'search', + options + ); // Nothing available, return the previous buckets. if (response.hits.total.value === 0) { @@ -45,7 +47,7 @@ export const getAllCompositeData = async < const newOptions = onAfterKey(options, response); return getAllCompositeData( framework, - request, + requestContext, newOptions, bucketSelector, onAfterKey, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 00945e12db51d..a1cf2ae4e8ead 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -3,7 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + SavedObjectsClientContract, +} from 'src/core/server'; import { Observable, combineLatest, AsyncSubject } from 'rxjs'; import { map } from 'rxjs/operators'; import { Server } from 'hapi'; @@ -11,6 +16,7 @@ import { once } from 'lodash'; import { Plugin as APMOSSPlugin } from '../../../../src/plugins/apm_oss/server'; import { createApmAgentConfigurationIndex } from '../../../legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index'; import { createApmApi } from '../../../legacy/plugins/apm/server/routes/create_apm_api'; +import { getApmIndices } from '../../../legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices'; import { APMConfig, mergeConfigs, APMXPackConfig } from '.'; export interface LegacySetup { @@ -20,13 +26,18 @@ export interface LegacySetup { export interface APMPluginContract { config$: Observable; registerLegacyAPI: (__LEGACY: LegacySetup) => void; + getApmIndices: ( + savedObjectsClient: SavedObjectsClientContract + ) => ReturnType; } export class APMPlugin implements Plugin { legacySetup$: AsyncSubject; + currentConfig: APMConfig; constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; this.legacySetup$ = new AsyncSubject(); + this.currentConfig = {} as APMConfig; } public async setup( @@ -49,6 +60,7 @@ export class APMPlugin implements Plugin { await new Promise(resolve => { combineLatest(mergedConfig$, core.elasticsearch.dataClient$).subscribe( async ([config, dataClient]) => { + this.currentConfig = config; await createApmAgentConfigurationIndex({ esClient: dataClient, config, @@ -64,6 +76,9 @@ export class APMPlugin implements Plugin { this.legacySetup$.next(__LEGACY); this.legacySetup$.complete(); }), + getApmIndices: async (savedObjectsClient: SavedObjectsClientContract) => { + return getApmIndices({ savedObjectsClient, config: this.currentConfig }); + }, }; } diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json new file mode 100644 index 0000000000000..b0670a58ae1e8 --- /dev/null +++ b/x-pack/plugins/infra/kibana.json @@ -0,0 +1,5 @@ +{ + "id": "infra", + "version": "8.0.0", + "server": true +} diff --git a/x-pack/plugins/infra/server/index.ts b/x-pack/plugins/infra/server/index.ts new file mode 100644 index 0000000000000..b12f92c8c5a9d --- /dev/null +++ b/x-pack/plugins/infra/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { InfraPlugin } from './plugin'; + +export const config = { + schema: schema.object({ + enabled: schema.maybe(schema.boolean()), + query: schema.object({ + partitionSize: schema.maybe(schema.number()), + partitionFactor: schema.maybe(schema.number()), + }), + }), +}; + +export const plugin = (initContext: PluginInitializerContext) => new InfraPlugin(initContext); + +export type InfraConfig = TypeOf; +export { InfraSetup } from './plugin'; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts new file mode 100644 index 0000000000000..0c763313fb973 --- /dev/null +++ b/x-pack/plugins/infra/server/plugin.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 { Plugin, PluginInitializerContext } from 'src/core/server'; + +export class InfraPlugin implements Plugin { + private readonly initContext: PluginInitializerContext; + + constructor(initContext: PluginInitializerContext) { + this.initContext = initContext; + } + + public setup() { + return { + __legacy: { + config: this.initContext.config, + }, + }; + } + + public start() {} + public stop() {} +} + +export interface InfraSetup { + /** @deprecated */ + __legacy: { + config: PluginInitializerContext['config']; + }; +} diff --git a/x-pack/test/api_integration/apis/infra/feature_controls.ts b/x-pack/test/api_integration/apis/infra/feature_controls.ts index 24d378d9b9a77..6556c309f31c5 100644 --- a/x-pack/test/api_integration/apis/infra/feature_controls.ts +++ b/x-pack/test/api_integration/apis/infra/feature_controls.ts @@ -19,7 +19,6 @@ const introspectionQuery = gql` `; export default function({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); const security = getService('security'); const spaces = getService('spaces'); const clientFactory = getService('infraOpsGraphQLClientFactory'); @@ -37,18 +36,6 @@ export default function({ getService }: FtrProviderContext) { expect(result.response.data).to.be.an('object'); }; - const expectGraphIQL404 = (result: any) => { - expect(result.error).to.be(undefined); - expect(result.response).not.to.be(undefined); - expect(result.response).to.have.property('statusCode', 404); - }; - - const expectGraphIQLResponse = (result: any) => { - expect(result.error).to.be(undefined); - expect(result.response).not.to.be(undefined); - expect(result.response).to.have.property('statusCode', 200); - }; - const executeGraphQLQuery = async (username: string, password: string, spaceId?: string) => { const queryOptions = { query: introspectionQuery, @@ -70,16 +57,6 @@ export default function({ getService }: FtrProviderContext) { }; }; - const executeGraphIQLRequest = async (username: string, password: string, spaceId?: string) => { - const basePath = spaceId ? `/s/${spaceId}` : ''; - - return supertest - .get(`${basePath}/api/infra/graphql/graphiql`) - .auth(username, password) - .then((response: any) => ({ error: undefined, response })) - .catch((error: any) => ({ error, response: undefined })); - }; - describe('feature controls', () => { it(`APIs can't be accessed by user with logstash-* "read" privileges`, async () => { const username = 'logstash_read'; @@ -105,9 +82,6 @@ export default function({ getService }: FtrProviderContext) { const graphQLResult = await executeGraphQLQuery(username, password); expectGraphQL404(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password); - expectGraphIQL404(graphQLIResult); } finally { await security.role.delete(roleName); await security.user.delete(username); @@ -144,9 +118,6 @@ export default function({ getService }: FtrProviderContext) { const graphQLResult = await executeGraphQLQuery(username, password); expectGraphQLResponse(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password); - expectGraphIQLResponse(graphQLIResult); } finally { await security.role.delete(roleName); await security.user.delete(username); @@ -186,9 +157,6 @@ export default function({ getService }: FtrProviderContext) { const graphQLResult = await executeGraphQLQuery(username, password); expectGraphQL404(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password); - expectGraphIQL404(graphQLIResult); } finally { await security.role.delete(roleName); await security.user.delete(username); @@ -268,25 +236,16 @@ export default function({ getService }: FtrProviderContext) { it('user_1 can access APIs in space_1', async () => { const graphQLResult = await executeGraphQLQuery(username, password, space1Id); expectGraphQLResponse(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password, space1Id); - expectGraphIQLResponse(graphQLIResult); }); it(`user_1 can access APIs in space_2`, async () => { const graphQLResult = await executeGraphQLQuery(username, password, space2Id); expectGraphQLResponse(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password, space2Id); - expectGraphIQLResponse(graphQLIResult); }); it(`user_1 can't access APIs in space_3`, async () => { const graphQLResult = await executeGraphQLQuery(username, password, space3Id); expectGraphQL404(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password, space3Id); - expectGraphIQL404(graphQLIResult); }); }); }); diff --git a/x-pack/test/typings/rison_node.d.ts b/x-pack/test/typings/rison_node.d.ts deleted file mode 100644 index ec8e5c1f407ad..0000000000000 --- a/x-pack/test/typings/rison_node.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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. - */ - -declare module 'rison-node' { - export type RisonValue = null | boolean | number | string | RisonObject | RisonArray; - - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface RisonArray extends Array {} - - export interface RisonObject { - [key: string]: RisonValue; - } - - export const decode: (input: string) => RisonValue; - - // eslint-disable-next-line @typescript-eslint/camelcase - export const decode_object: (input: string) => RisonObject; - - export const encode: (input: Input) => string; - - // eslint-disable-next-line @typescript-eslint/camelcase - export const encode_object: (input: Input) => string; -} From e80611483ff42344d61cacac97a6449347b72320 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 6 Dec 2019 10:53:06 -0800 Subject: [PATCH 16/35] State containers (#52384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add state containers * docs: ✏️ add state container demos * docs: ✏️ refrech state container docs * chore: 🤖 install default comparator * chore: 🤖 remove old state container implementation * feat: 🎸 add selectors * chore: 🤖 move Ensure tyep to type utils * fix: 🐛 fix useSelector() types and demo CLI command * test: 💍 add tests for state container demos * feat: 🎸 add ReacursiveReadonly to kbn-utility-types * feat: 🎸 shallow freeze state when not in production * test: 💍 fix Jest tests * refactor: 💡 remove .state and use BehaviourSubject --- package.json | 2 + packages/kbn-utility-types/README.md | 8 +- packages/kbn-utility-types/index.ts | 16 + src/plugins/kibana_utils/README.md | 2 +- src/plugins/kibana_utils/demos/demos.test.ts | 36 +++ .../state_containers/counter.ts} | 28 +- .../demos/state_containers/todomvc.ts | 69 ++++ .../docs/state_containers/README.md | 50 +++ .../{store => state_containers}/creation.md | 17 +- .../no_react.md} | 2 +- .../docs/state_containers/react.md | 41 +++ .../docs/state_containers/react/connect.md | 22 ++ .../docs/state_containers/react/context.md | 24 ++ .../state_containers/react/use_container.md | 10 + .../state_containers/react/use_selector.md | 20 ++ .../docs/state_containers/react/use_state.md | 11 + .../state_containers/react/use_transitions.md | 17 + .../docs/state_containers/redux.md | 40 +++ .../docs/state_containers/transitions.md | 61 ++++ src/plugins/kibana_utils/docs/store/README.md | 9 - .../kibana_utils/docs/store/mutators.md | 70 ---- src/plugins/kibana_utils/docs/store/react.md | 101 ------ src/plugins/kibana_utils/docs/store/redux.md | 19 -- src/plugins/kibana_utils/public/index.test.ts | 6 +- src/plugins/kibana_utils/public/index.ts | 9 +- .../create_state_container.test.ts | 303 ++++++++++++++++++ .../create_state_container.ts | 89 +++++ ...te_state_container_react_helpers.test.tsx} | 163 ++++++---- .../create_state_container_react_helpers.ts | 77 +++++ .../{store => state_containers}/index.ts | 5 +- .../public/state_containers/types.ts | 99 ++++++ .../public/store/create_store.test.ts | 177 ---------- .../kibana_utils/public/store/create_store.ts | 85 ----- .../public/store/observable_selector.ts | 47 --- .../kibana_utils/public/store/react.ts | 126 -------- yarn.lock | 164 +++++++++- 36 files changed, 1275 insertions(+), 750 deletions(-) create mode 100644 src/plugins/kibana_utils/demos/demos.test.ts rename src/plugins/kibana_utils/{public/store/types.ts => demos/state_containers/counter.ts} (51%) create mode 100644 src/plugins/kibana_utils/demos/state_containers/todomvc.ts create mode 100644 src/plugins/kibana_utils/docs/state_containers/README.md rename src/plugins/kibana_utils/docs/{store => state_containers}/creation.md (54%) rename src/plugins/kibana_utils/docs/{store/getters.md => state_containers/no_react.md} (83%) create mode 100644 src/plugins/kibana_utils/docs/state_containers/react.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/connect.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/context.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/use_container.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/use_selector.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/use_state.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/use_transitions.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/redux.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/transitions.md delete mode 100644 src/plugins/kibana_utils/docs/store/README.md delete mode 100644 src/plugins/kibana_utils/docs/store/mutators.md delete mode 100644 src/plugins/kibana_utils/docs/store/react.md delete mode 100644 src/plugins/kibana_utils/docs/store/redux.md create mode 100644 src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts create mode 100644 src/plugins/kibana_utils/public/state_containers/create_state_container.ts rename src/plugins/kibana_utils/public/{store/react.test.tsx => state_containers/create_state_container_react_helpers.test.tsx} (65%) create mode 100644 src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts rename src/plugins/kibana_utils/public/{store => state_containers}/index.ts (86%) create mode 100644 src/plugins/kibana_utils/public/state_containers/types.ts delete mode 100644 src/plugins/kibana_utils/public/store/create_store.test.ts delete mode 100644 src/plugins/kibana_utils/public/store/create_store.ts delete mode 100644 src/plugins/kibana_utils/public/store/observable_selector.ts delete mode 100644 src/plugins/kibana_utils/public/store/react.ts diff --git a/package.json b/package.json index 2b157da779f63..847f09b4ab4cf 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,7 @@ "encode-uri-query": "1.0.1", "execa": "^3.2.0", "expiry-js": "0.1.7", + "fast-deep-equal": "^3.1.1", "file-loader": "4.2.0", "font-awesome": "4.7.0", "getos": "^3.1.0", @@ -229,6 +230,7 @@ "react-resize-detector": "^4.2.0", "react-router-dom": "^4.3.1", "react-sizeme": "^2.3.6", + "react-use": "^13.10.2", "reactcss": "1.2.3", "redux": "4.0.0", "redux-actions": "2.2.1", diff --git a/packages/kbn-utility-types/README.md b/packages/kbn-utility-types/README.md index ff6c7c7268a15..9707ff5a1ed9c 100644 --- a/packages/kbn-utility-types/README.md +++ b/packages/kbn-utility-types/README.md @@ -18,7 +18,9 @@ type B = UnwrapPromise
; // string ## Reference -- `UnwrapPromise` — Returns wrapped type of a promise. -- `UnwrapObservable` — Returns wrapped type of an observable. -- `ShallowPromise` — Same as `Promise` type, but it flat maps the wrapped type. +- `Ensure` — Makes sure `T` is of type `X`. - `ObservableLike` — Minimal interface for an object resembling an `Observable`. +- `RecursiveReadonly` — Like `Readonly`, but freezes object recursively. +- `ShallowPromise` — Same as `Promise` type, but it flat maps the wrapped type. +- `UnwrapObservable` — Returns wrapped type of an observable. +- `UnwrapPromise` — Returns wrapped type of a promise. diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index f17890528bfd2..495b5fb374b43 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -42,3 +42,19 @@ export type UnwrapObservable> = T extends Observab * Converts a type to a `Promise`, unless it is already a `Promise`. Useful when proxying the return value of a possibly async function. */ export type ShallowPromise = T extends Promise ? Promise : Promise; + +/** + * Ensures T is of type X. + */ +export type Ensure = T extends X ? T : never; + +// If we define this inside RecursiveReadonly TypeScript complains. +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface RecursiveReadonlyArray extends Array> {} +export type RecursiveReadonly = T extends (...args: any) => any + ? T + : T extends any[] + ? RecursiveReadonlyArray + : T extends object + ? Readonly<{ [K in keyof T]: RecursiveReadonly }> + : T; diff --git a/src/plugins/kibana_utils/README.md b/src/plugins/kibana_utils/README.md index 61ceea2b18385..5501505dbb7e2 100644 --- a/src/plugins/kibana_utils/README.md +++ b/src/plugins/kibana_utils/README.md @@ -2,4 +2,4 @@ Utilities for building Kibana plugins. -- [Store reactive serializable app state in state containers, `createStore`](./docs/store/README.md). +- [State containers](./docs/state_containers/README.md). diff --git a/src/plugins/kibana_utils/demos/demos.test.ts b/src/plugins/kibana_utils/demos/demos.test.ts new file mode 100644 index 0000000000000..4e792ceef117a --- /dev/null +++ b/src/plugins/kibana_utils/demos/demos.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { result as counterResult } from './state_containers/counter'; +import { result as todomvcResult } from './state_containers/todomvc'; + +describe('demos', () => { + describe('state containers', () => { + test('counter demo works', () => { + expect(counterResult).toBe(10); + }); + + test('TodoMVC demo works', () => { + expect(todomvcResult).toEqual([ + { id: 0, text: 'Learning state containers', completed: true }, + { id: 1, text: 'Learning transitions...', completed: true }, + ]); + }); + }); +}); diff --git a/src/plugins/kibana_utils/public/store/types.ts b/src/plugins/kibana_utils/demos/state_containers/counter.ts similarity index 51% rename from src/plugins/kibana_utils/public/store/types.ts rename to src/plugins/kibana_utils/demos/state_containers/counter.ts index 952ee07f18baf..643763cc4cee9 100644 --- a/src/plugins/kibana_utils/public/store/types.ts +++ b/src/plugins/kibana_utils/demos/state_containers/counter.ts @@ -17,26 +17,16 @@ * under the License. */ -import { Observable } from 'rxjs'; -import { Store as ReduxStore } from 'redux'; +import { createStateContainer } from '../../public/state_containers'; -export interface AppStore< - State extends {}, - StateMutators extends Mutators> = {} -> { - redux: ReduxStore; - get: () => State; - set: (state: State) => void; - state$: Observable; - createMutators: >(pureMutators: M) => Mutators; - mutators: StateMutators; -} +const container = createStateContainer(0, { + increment: (cnt: number) => (by: number) => cnt + by, + double: (cnt: number) => () => cnt * 2, +}); -export type PureMutator = (state: State) => (...args: any[]) => State; -export type Mutator> = (...args: Parameters>) => void; +container.transitions.increment(5); +container.transitions.double(); -export interface PureMutators { - [name: string]: PureMutator; -} +console.log(container.get()); // eslint-disable-line -export type Mutators> = { [K in keyof M]: Mutator }; +export const result = container.get(); diff --git a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts new file mode 100644 index 0000000000000..6d0c960e2a5b2 --- /dev/null +++ b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts @@ -0,0 +1,69 @@ +/* + * 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 { createStateContainer, PureTransition } from '../../public/state_containers'; + +export interface TodoItem { + text: string; + completed: boolean; + id: number; +} + +export type TodoState = TodoItem[]; + +export const defaultState: TodoState = [ + { + id: 0, + text: 'Learning state containers', + completed: false, + }, +]; + +export interface TodoActions { + add: PureTransition; + edit: PureTransition; + delete: PureTransition; + complete: PureTransition; + completeAll: PureTransition; + clearCompleted: PureTransition; +} + +export const pureTransitions: TodoActions = { + add: state => todo => [...state, todo], + edit: state => todo => state.map(item => (item.id === todo.id ? { ...item, ...todo } : item)), + delete: state => id => state.filter(item => item.id !== id), + complete: state => id => + state.map(item => (item.id === id ? { ...item, completed: true } : item)), + completeAll: state => () => state.map(item => ({ ...item, completed: true })), + clearCompleted: state => () => state.filter(({ completed }) => !completed), +}; + +const container = createStateContainer(defaultState, pureTransitions); + +container.transitions.add({ + id: 1, + text: 'Learning transitions...', + completed: false, +}); +container.transitions.complete(0); +container.transitions.complete(1); + +console.log(container.get()); // eslint-disable-line + +export const result = container.get(); diff --git a/src/plugins/kibana_utils/docs/state_containers/README.md b/src/plugins/kibana_utils/docs/state_containers/README.md new file mode 100644 index 0000000000000..3b7a8b8bd4621 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/README.md @@ -0,0 +1,50 @@ +# State containers + +State containers are Redux-store-like objects meant to help you manage state in +your services or apps. + +- State containers are strongly typed, you will get TypeScript autocompletion suggestions from + your editor when accessing state, executing transitions and using React helpers. +- State containers can be easily hooked up with your React components. +- State containers can be used without React, too. +- State containers provide you central place where to store state, instead of spreading + state around multiple RxJs observables, which you need to coordinate. With state + container you can always access the latest state snapshot synchronously. +- Unlike Redux, state containers are less verbose, see example below. + + +## Example + +```ts +import { createStateContainer } from 'src/plugins/kibana_utils'; + +const container = createStateContainer(0, { + increment: (cnt: number) => (by: number) => cnt + by, + double: (cnt: number) => () => cnt * 2, +}); + +container.transitions.increment(5); +container.transitions.double(); +console.log(container.get()); // 10 +``` + + +## Demos + +See demos [here](../../demos/state_containers/). + +You can run them with + +``` +npx -q ts-node src/plugins/kibana_utils/demos/state_containers/counter.ts +npx -q ts-node src/plugins/kibana_utils/demos/state_containers/todomvc.ts +``` + + +## Reference + +- [Creating a state container](./creation.md). +- [State transitions](./transitions.md). +- [Using with React](./react.md). +- [Using without React`](./no_react.md). +- [Parallels with Redux](./redux.md). diff --git a/src/plugins/kibana_utils/docs/store/creation.md b/src/plugins/kibana_utils/docs/state_containers/creation.md similarity index 54% rename from src/plugins/kibana_utils/docs/store/creation.md rename to src/plugins/kibana_utils/docs/state_containers/creation.md index b0184ad45eb84..66d28bbd8603f 100644 --- a/src/plugins/kibana_utils/docs/store/creation.md +++ b/src/plugins/kibana_utils/docs/state_containers/creation.md @@ -17,7 +17,7 @@ interface MyState { } ``` -Create default state of your *store*. +Create default state of your container. ```ts const defaultState: MyState = { @@ -27,17 +27,12 @@ const defaultState: MyState = { }; ``` -Create your state container, i.e *store*. +Create your a state container. ```ts -import { createStore } from 'kibana-utils'; +import { createStateContainer } from 'src/plugins/kibana_utils'; -const store = createStore(defaultState); -console.log(store.get()); -``` +const container = createStateContainer(defaultState, {}); -> ##### N.B. -> -> State must always be an object `{}`. -> -> You cannot create a store out of an array, e.g ~~`createStore([])`~~. +console.log(container.get()); +``` diff --git a/src/plugins/kibana_utils/docs/store/getters.md b/src/plugins/kibana_utils/docs/state_containers/no_react.md similarity index 83% rename from src/plugins/kibana_utils/docs/store/getters.md rename to src/plugins/kibana_utils/docs/state_containers/no_react.md index 508d0c6ebc18d..7a15483d83b44 100644 --- a/src/plugins/kibana_utils/docs/store/getters.md +++ b/src/plugins/kibana_utils/docs/state_containers/no_react.md @@ -1,4 +1,4 @@ -# Reading state +# Consuming state in non-React setting To read the current `state` of the store use `.get()` method. diff --git a/src/plugins/kibana_utils/docs/state_containers/react.md b/src/plugins/kibana_utils/docs/state_containers/react.md new file mode 100644 index 0000000000000..363fd9253d44f --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react.md @@ -0,0 +1,41 @@ +# React + +`createStateContainerReactHelpers` factory allows you to easily use state containers with React. + + +## Example + + +```ts +import { createStateContainer, createStateContainerReactHelpers } from 'src/plugins/kibana_utils'; + +const container = createStateContainer({}, {}); +export const { + Provider, + Consumer, + context, + useContainer, + useState, + useTransitions, + useSelector, + connect, +} = createStateContainerReactHelpers(); +``` + +Wrap your app with ``. + +```tsx + + + +``` + + +## Reference + +- [`useContainer()`](./react/use_container.md) +- [`useState()`](./react/use_state.md) +- [`useSelector()`](./react/use_selector.md) +- [`useTransitions()`](./react/use_transitions.md) +- [`connect()()`](./react/connect.md) +- [Context](./react/context.md) diff --git a/src/plugins/kibana_utils/docs/state_containers/react/connect.md b/src/plugins/kibana_utils/docs/state_containers/react/connect.md new file mode 100644 index 0000000000000..56b7e0fbc5673 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/connect.md @@ -0,0 +1,22 @@ +# `connect()()` higher order component + +Use `connect()()` higher-order-component to inject props from state into your component. + +```tsx +interface Props { + name: string; + punctuation: '.' | ',' | '!', +} +const Demo: React.FC = ({ name, punctuation }) => +
Hello, {name}{punctuation}
; + +const store = createStateContainer({ userName: 'John' }); +const { Provider, connect } = createStateContainerReactHelpers(store); + +const mapStateToProps = ({ userName }) => ({ name: userName }); +const DemoConnected = connect(mapStateToProps)(Demo); + + + + +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/react/context.md b/src/plugins/kibana_utils/docs/state_containers/react/context.md new file mode 100644 index 0000000000000..33f084fdfe9d7 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/context.md @@ -0,0 +1,24 @@ +# React context + +`createStateContainerReactHelpers` returns `` and `` components +as well as `context` React context object. + +```ts +export const { + Provider, + Consumer, + context, +} = createStateContainerReactHelpers(); +``` + +`` and `` are just regular React context components. + +```tsx + +
+ {container => +
{JSON.stringify(container.get())}
+ }
+
+
+``` diff --git a/src/plugins/kibana_utils/docs/state_containers/react/use_container.md b/src/plugins/kibana_utils/docs/state_containers/react/use_container.md new file mode 100644 index 0000000000000..5e698edb8529c --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/use_container.md @@ -0,0 +1,10 @@ +# `useContainer` hook + +`useContainer` React hook will simply return you `container` object from React context. + +```tsx +const Demo = () => { + const store = useContainer(); + return
{store.get().isDarkMode ? '🌑' : '☀️'}
; +}; +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/react/use_selector.md b/src/plugins/kibana_utils/docs/state_containers/react/use_selector.md new file mode 100644 index 0000000000000..2ecf772fba367 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/use_selector.md @@ -0,0 +1,20 @@ +# `useSelector()` hook + +With `useSelector` React hook you specify a selector function, which will pick specific +data from the state. *Your component will update only when that specific part of the state changes.* + +```tsx +const selector = state => state.isDarkMode; +const Demo = () => { + const isDarkMode = useSelector(selector); + return
{isDarkMode ? '🌑' : '☀️'}
; +}; +``` + +As an optional second argument for `useSelector` you can provide a `comparator` function, which +compares currently selected value with the previous and your component will re-render only if +`comparator` returns `true`. By default it uses [`fast-deep-equal`](https://github.com/epoberezkin/fast-deep-equal). + +``` +useSelector(selector, comparator?) +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/react/use_state.md b/src/plugins/kibana_utils/docs/state_containers/react/use_state.md new file mode 100644 index 0000000000000..5db1d46897aad --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/use_state.md @@ -0,0 +1,11 @@ +# `useState()` hook + +- `useState` hook returns you directly the state of the container. +- It also forces component to re-render every time state changes. + +```tsx +const Demo = () => { + const { isDarkMode } = useState(); + return
{isDarkMode ? '🌑' : '☀️'}
; +}; +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/react/use_transitions.md b/src/plugins/kibana_utils/docs/state_containers/react/use_transitions.md new file mode 100644 index 0000000000000..c6783bf0e0f0a --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/use_transitions.md @@ -0,0 +1,17 @@ +# `useTransitions` hook + +Access [state transitions](../transitions.md) by `useTransitions` React hook. + +```tsx +const Demo = () => { + const { isDarkMode } = useState(); + const { setDarkMode } = useTransitions(); + return ( + <> +
{isDarkMode ? '🌑' : '☀️'}
+ + + + ); +}; +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/redux.md b/src/plugins/kibana_utils/docs/state_containers/redux.md new file mode 100644 index 0000000000000..1a60d841a8b75 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/redux.md @@ -0,0 +1,40 @@ +# Redux + +State containers similar to Redux stores but without the boilerplate. + +State containers expose Redux-like API: + +```js +container.getState() +container.dispatch() +container.replaceReducer() +container.subscribe() +container.addMiddleware() +``` + +State containers have a reducer and every time you execute a state transition it +actually dispatches an "action". For example, this + +```js +container.transitions.increment(25); +``` + +is equivalent to + +```js +container.dispatch({ + type: 'increment', + args: [25], +}); +``` + +Because all transitions happen through `.dispatch()` interface, you can add middleware—similar how you +would do with Redux—to monitor or intercept transitions. + +For example, you can add `redux-logger` middleware to log in console all transitions happening with your store. + +```js +import logger from 'redux-logger'; + +container.addMiddleware(logger); +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/transitions.md b/src/plugins/kibana_utils/docs/state_containers/transitions.md new file mode 100644 index 0000000000000..51d52cdf3daaf --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/transitions.md @@ -0,0 +1,61 @@ +# State transitions + +*State transitions* describe possible state changes over time. Transitions are pure functions which +receive `state` object and other—optional—arguments and must return a new `state` object back. + +```ts +type Transition = (state: State) => (...args) => State; +``` + +Transitions must not mutate `state` object in-place, instead they must return a +shallow copy of it, e.g. `{ ...state }`. Example: + +```ts +const setUiMode: PureTransition = state => uiMode => ({ ...state, uiMode }); +``` + +You provide transitions as a second argument when you create your state container. + +```ts +import { createStateContainer } from 'src/plugins/kibana_utils'; + +const container = createStateContainer(0, { + increment: (cnt: number) => (by: number) => cnt + by, + double: (cnt: number) => () => cnt * 2, +}); +``` + +Now you can execute the transitions by calling them with only optional parameters (`state` is +provided to your transitions automatically). + +```ts +container.transitions.increment(25); +container.transitions.increment(5); +container.state; // 30 +``` + +Your transitions are bound to the container so you can treat each of them as a +standalone function for export. + +```ts +const defaultState = { + uiMode: 'light', +}; + +const container = createStateContainer(defaultState, { + setUiMode: state => uiMode => ({ ...state, uiMode }), + resetUiMode: state => () => ({ ...state, uiMode: defaultState.uiMode }), +}); + +export const { + setUiMode, + resetUiMode +} = container.transitions; +``` + +You can add TypeScript annotations for your transitions as the second generic argument +to `createStateContainer()` function. + +```ts +const container = createStateContainer(defaultState, pureTransitions); +``` diff --git a/src/plugins/kibana_utils/docs/store/README.md b/src/plugins/kibana_utils/docs/store/README.md deleted file mode 100644 index e1cb098fe04ce..0000000000000 --- a/src/plugins/kibana_utils/docs/store/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# State containers - -- State containers for holding serializable state. -- [Each plugin/app that needs runtime state will create a *store* using `store = createStore()`](./creation.md). -- [*Store* can be updated using mutators `mutators = store.createMutators({ ... })`](./mutators.md). -- [*Store* can be connected to React `{Provider, connect} = createContext(store)`](./react.md). -- [In no-React setting *store* is consumed using `store.get()` and `store.state$`](./getters.md). -- [Under-the-hood uses Redux `store.redux`](./redux.md) (but you should never need it explicitly). -- [See idea doc with samples and rationale](https://docs.google.com/document/d/18eitHkcyKSsEHUfUIqFKChc8Pp62Z4gcRxdu903hbA0/edit#heading=h.iaxc9whxifl5). diff --git a/src/plugins/kibana_utils/docs/store/mutators.md b/src/plugins/kibana_utils/docs/store/mutators.md deleted file mode 100644 index 9db1b1bb60b3c..0000000000000 --- a/src/plugins/kibana_utils/docs/store/mutators.md +++ /dev/null @@ -1,70 +0,0 @@ -# Mutators - -State *mutators* are pure functions which receive `state` object and other—optional—arguments -and must return a new `state` object back. - -```ts -type Mutator = (state: State) => (...args) => State; -``` - -Mutator must not mutate `state` object in-place, instead it should return a -shallow copy of it, e.g. `{ ...state }`. - -```ts -const setUiMode: Mutator = state => uiMode => ({ ...state, uiMode }); -``` - -You create mutators using `.createMutator(...)` method. - -```ts -const store = createStore({uiMode: 'light'}); -const mutators = store.createMutators({ - setUiMode: state => uiMode => ({ ...state, uiMode }), -}); -``` - -Now you can use your mutators by calling them with only optional parameters (`state` is -provided to your mutator automatically). - -```ts -mutators.setUiMode('dark'); -``` - -Your mutators are bound to the `store` so you can treat each of them as a -standalone function for export. - -```ts -const { setUiMode, resetUiMode } = store.createMutators({ - setUiMode: state => uiMode => ({ ...state, uiMode }), - resetUiMode: state => () => ({ ...state, uiMode: 'light' }), -}); - -export { - setUiMode, - resetUiMode, -}; -``` - -The mutators you create are also available on the `store` object. - -```ts -const store = createStore({ cnt: 0 }); -store.createMutators({ - add: state => value => ({ ...state, cnt: state.cnt + value }), -}); - -store.mutators.add(5); -store.get(); // { cnt: 5 } -``` - -You can add TypeScript annotations to your `.mutators` property of `store` object. - -```ts -const store = createStore<{ - cnt: number; -}, { - add: (value: number) => void; -}>({ - cnt: 0 -}); -``` diff --git a/src/plugins/kibana_utils/docs/store/react.md b/src/plugins/kibana_utils/docs/store/react.md deleted file mode 100644 index 68a016ed6d3ca..0000000000000 --- a/src/plugins/kibana_utils/docs/store/react.md +++ /dev/null @@ -1,101 +0,0 @@ -# React - -`createContext` factory allows you to easily use state containers with React. - -```ts -import { createStore, createContext } from 'kibana-utils'; - -const store = createStore({}); -const { - Provider, - Consumer, - connect, - context, - useStore, - useState, - useMutators, - useSelector, -} = createContext(store); -``` - -Wrap your app with ``. - -```tsx - - - -``` - -Use `connect()()` higer-order-component to inject props from state into your component. - -```tsx -interface Props { - name: string; - punctuation: '.' | ',' | '!', -} -const Demo: React.FC = ({ name, punctuation }) => -
Hello, {name}{punctuation}
; - -const store = createStore({ userName: 'John' }); -const { Provider, connect } = createContext(store); - -const mapStateToProps = ({ userName }) => ({ name: userName }); -const DemoConnected = connect(mapStateToProps)(Demo); - - - - -``` - -`useStore` React hook will fetch the `store` object from the context. - -```tsx -const Demo = () => { - const store = useStore(); - return
{store.get().isDarkMode ? '🌑' : '☀️'}
; -}; -``` - -If you want your component to always re-render when the state changes use `useState` React hook. - -```tsx -const Demo = () => { - const { isDarkMode } = useState(); - return
{isDarkMode ? '🌑' : '☀️'}
; -}; -``` - -For `useSelector` React hook you specify a selector function, which will pick specific -data from the state. *Your component will update only when that specific part of the state changes.* - -```tsx -const selector = state => state.isDarkMode; -const Demo = () => { - const isDarkMode = useSelector(selector); - return
{isDarkMode ? '🌑' : '☀️'}
; -}; -``` - -As an optional second argument for `useSelector` you can provide a `comparator` function, which -compares currently selected value with the previous and your component will re-render only if -`comparator` returns `true`. By default, it simply uses tripple equals `===` comparison. - -``` -useSelector(selector, comparator?) -``` - -Access state mutators by `useMutators` React hook. - -```tsx -const Demo = () => { - const { isDarkMode } = useState(); - const { setDarkMode } = useMutators(); - return ( - <> -
{isDarkMode ? '🌑' : '☀️'}
- - - - ); -}; -``` diff --git a/src/plugins/kibana_utils/docs/store/redux.md b/src/plugins/kibana_utils/docs/store/redux.md deleted file mode 100644 index 23be76f35b36e..0000000000000 --- a/src/plugins/kibana_utils/docs/store/redux.md +++ /dev/null @@ -1,19 +0,0 @@ -# Redux - -Internally `createStore()` uses Redux to manage the state. When you call `store.get()` -it is actually calling the Redux `.getState()` method. When you execute a mutation -it is actually dispatching a Redux action. - -You can access Redux *store* using `.redux`. - -```ts -store.redux; -``` - -But you should never need it, if you think you do, consult with Kibana App Architecture team. - -We use Redux internally for 3 main reasons: - -- We can reuse `react-redux` library to easily connect state containers to React. -- We can reuse Redux devtools. -- We can reuse battle-tested Redux library and action/reducer paradigm. diff --git a/src/plugins/kibana_utils/public/index.test.ts b/src/plugins/kibana_utils/public/index.test.ts index 0e2a4acf15f04..27c4d6c1c06e9 100644 --- a/src/plugins/kibana_utils/public/index.test.ts +++ b/src/plugins/kibana_utils/public/index.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { createStore, createContext } from '.'; +import { createStateContainer, createStateContainerReactHelpers } from '.'; test('exports store methods', () => { - expect(typeof createStore).toBe('function'); - expect(typeof createContext).toBe('function'); + expect(typeof createStateContainer).toBe('function'); + expect(typeof createStateContainerReactHelpers).toBe('function'); }); diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index c5c129eca8fd3..3f5aeebac54d8 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -19,13 +19,12 @@ export * from './core'; export * from './errors'; -export * from './store'; -export * from './parse'; -export * from './resize_checker'; -export * from './render_complete'; -export * from './store'; export * from './errors'; export * from './field_mapping'; +export * from './parse'; +export * from './render_complete'; +export * from './resize_checker'; +export * from './state_containers'; export * from './storage'; export * from './storage/hashed_item_store'; export * from './state_management/state_hash'; diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts new file mode 100644 index 0000000000000..9165181299a90 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts @@ -0,0 +1,303 @@ +/* + * 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 { createStateContainer } from './create_state_container'; + +const create = (state: S, transitions: T = {} as T) => { + const pureTransitions = { + set: () => (newState: S) => newState, + ...transitions, + }; + const store = createStateContainer(state, pureTransitions); + return { store, mutators: store.transitions }; +}; + +test('can create store', () => { + const { store } = create({}); + expect(store).toMatchObject({ + getState: expect.any(Function), + state$: expect.any(Object), + transitions: expect.any(Object), + dispatch: expect.any(Function), + subscribe: expect.any(Function), + replaceReducer: expect.any(Function), + addMiddleware: expect.any(Function), + }); +}); + +test('can set default state', () => { + const defaultState = { + foo: 'bar', + }; + const { store } = create(defaultState); + expect(store.get()).toEqual(defaultState); + expect(store.getState()).toEqual(defaultState); +}); + +test('can set state', () => { + const defaultState = { + foo: 'bar', + }; + const newState = { + foo: 'baz', + }; + const { store, mutators } = create(defaultState); + + mutators.set(newState); + + expect(store.get()).toEqual(newState); + expect(store.getState()).toEqual(newState); +}); + +test('does not shallow merge states', () => { + const defaultState = { + foo: 'bar', + }; + const newState = { + foo2: 'baz', + }; + const { store, mutators } = create(defaultState); + + mutators.set(newState as any); + + expect(store.get()).toEqual(newState); + expect(store.getState()).toEqual(newState); +}); + +test('can subscribe and unsubscribe to state changes', () => { + const { store, mutators } = create({}); + const spy = jest.fn(); + const subscription = store.state$.subscribe(spy); + mutators.set({ a: 1 }); + mutators.set({ a: 2 }); + subscription.unsubscribe(); + mutators.set({ a: 3 }); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy.mock.calls[0][0]).toEqual({ a: 1 }); + expect(spy.mock.calls[1][0]).toEqual({ a: 2 }); +}); + +test('multiple subscribers can subscribe', () => { + const { store, mutators } = create({}); + const spy1 = jest.fn(); + const spy2 = jest.fn(); + const subscription1 = store.state$.subscribe(spy1); + const subscription2 = store.state$.subscribe(spy2); + mutators.set({ a: 1 }); + subscription1.unsubscribe(); + mutators.set({ a: 2 }); + subscription2.unsubscribe(); + mutators.set({ a: 3 }); + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(2); + expect(spy1.mock.calls[0][0]).toEqual({ a: 1 }); + expect(spy2.mock.calls[0][0]).toEqual({ a: 1 }); + expect(spy2.mock.calls[1][0]).toEqual({ a: 2 }); +}); + +test('creates impure mutators from pure mutators', () => { + const { mutators } = create( + {}, + { + setFoo: () => (bar: any) => ({ foo: bar }), + } + ); + + expect(typeof mutators.setFoo).toBe('function'); +}); + +test('mutators can update state', () => { + const { store, mutators } = create( + { + value: 0, + foo: 'bar', + }, + { + add: (state: any) => (increment: any) => ({ ...state, value: state.value + increment }), + setFoo: (state: any) => (bar: any) => ({ ...state, foo: bar }), + } + ); + + expect(store.get()).toEqual({ + value: 0, + foo: 'bar', + }); + + mutators.add(11); + mutators.setFoo('baz'); + + expect(store.get()).toEqual({ + value: 11, + foo: 'baz', + }); + + mutators.add(-20); + mutators.setFoo('bazooka'); + + expect(store.get()).toEqual({ + value: -9, + foo: 'bazooka', + }); +}); + +test('mutators methods are not bound', () => { + const { store, mutators } = create( + { value: -3 }, + { + add: (state: { value: number }) => (increment: number) => ({ + ...state, + value: state.value + increment, + }), + } + ); + + expect(store.get()).toEqual({ value: -3 }); + mutators.add(4); + expect(store.get()).toEqual({ value: 1 }); +}); + +test('created mutators are saved in store object', () => { + const { store, mutators } = create( + { value: -3 }, + { + add: (state: { value: number }) => (increment: number) => ({ + ...state, + value: state.value + increment, + }), + } + ); + + expect(typeof store.transitions.add).toBe('function'); + mutators.add(5); + expect(store.get()).toEqual({ value: 2 }); +}); + +test('throws when state is modified inline - 1', () => { + const container = createStateContainer({ a: 'b' }, {}); + + let error: TypeError | null = null; + try { + (container.get().a as any) = 'c'; + } catch (err) { + error = err; + } + + expect(error).toBeInstanceOf(TypeError); +}); + +test('throws when state is modified inline - 2', () => { + const container = createStateContainer({ a: 'b' }, {}); + + let error: TypeError | null = null; + try { + (container.getState().a as any) = 'c'; + } catch (err) { + error = err; + } + + expect(error).toBeInstanceOf(TypeError); +}); + +test('throws when state is modified inline in subscription', done => { + const container = createStateContainer({ a: 'b' }, { set: () => (newState: any) => newState }); + + container.subscribe(value => { + let error: TypeError | null = null; + try { + (value.a as any) = 'd'; + } catch (err) { + error = err; + } + expect(error).toBeInstanceOf(TypeError); + done(); + }); + container.transitions.set({ a: 'c' }); +}); + +describe('selectors', () => { + test('can specify no selectors, or can skip them', () => { + createStateContainer({}, {}); + createStateContainer({}, {}, {}); + }); + + test('selector object is available on .selectors key', () => { + const container1 = createStateContainer({}, {}, {}); + const container2 = createStateContainer({}, {}, { foo: () => () => 123 }); + const container3 = createStateContainer({}, {}, { bar: () => () => 1, baz: () => () => 1 }); + + expect(Object.keys(container1.selectors).sort()).toEqual([]); + expect(Object.keys(container2.selectors).sort()).toEqual(['foo']); + expect(Object.keys(container3.selectors).sort()).toEqual(['bar', 'baz']); + }); + + test('selector without arguments returns correct state slice', () => { + const container = createStateContainer( + { name: 'Oleg' }, + { + changeName: (state: { name: string }) => (name: string) => ({ ...state, name }), + }, + { getName: (state: { name: string }) => () => state.name } + ); + + expect(container.selectors.getName()).toBe('Oleg'); + container.transitions.changeName('Britney'); + expect(container.selectors.getName()).toBe('Britney'); + }); + + test('selector can accept an argument', () => { + const container = createStateContainer( + { + users: { + 1: { + name: 'Darth', + }, + }, + }, + {}, + { + getUser: (state: any) => (id: number) => state.users[id], + } + ); + + expect(container.selectors.getUser(1)).toEqual({ name: 'Darth' }); + expect(container.selectors.getUser(2)).toBe(undefined); + }); + + test('selector can accept multiple arguments', () => { + const container = createStateContainer( + { + users: { + 5: { + name: 'Darth', + surname: 'Vader', + }, + }, + }, + {}, + { + getName: (state: any) => (id: number, which: 'name' | 'surname') => state.users[id][which], + } + ); + + expect(container.selectors.getName(5, 'name')).toEqual('Darth'); + expect(container.selectors.getName(5, 'surname')).toEqual('Vader'); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container.ts new file mode 100644 index 0000000000000..1ef4a1c012817 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container.ts @@ -0,0 +1,89 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import { skip } from 'rxjs/operators'; +import { RecursiveReadonly } from '@kbn/utility-types'; +import { + PureTransitionsToTransitions, + PureTransition, + ReduxLikeStateContainer, + PureSelectorsToSelectors, +} from './types'; + +const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable'; + +const freeze: (value: T) => RecursiveReadonly = + process.env.NODE_ENV !== 'production' + ? (value: T): RecursiveReadonly => { + if (!value) return value as RecursiveReadonly; + if (value instanceof Array) return value as RecursiveReadonly; + if (typeof value === 'object') return Object.freeze({ ...value }) as RecursiveReadonly; + else return value as RecursiveReadonly; + } + : (value: T) => value as RecursiveReadonly; + +export const createStateContainer = < + State, + PureTransitions extends object, + PureSelectors extends object = {} +>( + defaultState: State, + pureTransitions: PureTransitions, + pureSelectors: PureSelectors = {} as PureSelectors +): ReduxLikeStateContainer => { + const data$ = new BehaviorSubject>(freeze(defaultState)); + const state$ = data$.pipe(skip(1)); + const get = () => data$.getValue(); + const container: ReduxLikeStateContainer = { + get, + state$, + getState: () => data$.getValue(), + set: (state: State) => { + data$.next(freeze(state)); + }, + reducer: (state, action) => { + const pureTransition = (pureTransitions as Record>)[ + action.type + ]; + return pureTransition ? freeze(pureTransition(state)(...action.args)) : state; + }, + replaceReducer: nextReducer => (container.reducer = nextReducer), + dispatch: action => data$.next(container.reducer(get(), action)), + transitions: Object.keys(pureTransitions).reduce>( + (acc, type) => ({ ...acc, [type]: (...args: any) => container.dispatch({ type, args }) }), + {} as PureTransitionsToTransitions + ), + selectors: Object.keys(pureSelectors).reduce>( + (acc, selector) => ({ + ...acc, + [selector]: (...args: any) => (pureSelectors as any)[selector](get())(...args), + }), + {} as PureSelectorsToSelectors + ), + addMiddleware: middleware => + (container.dispatch = middleware(container as any)(container.dispatch)), + subscribe: (listener: (state: RecursiveReadonly) => void) => { + const subscription = state$.subscribe(listener); + return () => subscription.unsubscribe(); + }, + [$$observable]: state$, + }; + return container; +}; diff --git a/src/plugins/kibana_utils/public/store/react.test.tsx b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx similarity index 65% rename from src/plugins/kibana_utils/public/store/react.test.tsx rename to src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx index e629e9d0e1257..8f5810f3e147d 100644 --- a/src/plugins/kibana_utils/public/store/react.test.tsx +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx @@ -20,8 +20,17 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { act, Simulate } from 'react-dom/test-utils'; -import { createStore } from './create_store'; -import { createContext } from './react'; +import { createStateContainer } from './create_state_container'; +import { createStateContainerReactHelpers } from './create_state_container_react_helpers'; + +const create = (state: S, transitions: T = {} as T) => { + const pureTransitions = { + set: () => (newState: S) => newState, + ...transitions, + }; + const store = createStateContainer(state, pureTransitions); + return { store, mutators: store.transitions }; +}; let container: HTMLDivElement | null; @@ -36,27 +45,23 @@ afterEach(() => { }); test('can create React context', () => { - const store = createStore({ foo: 'bar' }); - const context = createContext(store); + const context = createStateContainerReactHelpers(); expect(context).toMatchObject({ - Provider: expect.any(Function), - Consumer: expect.any(Function), + Provider: expect.any(Object), + Consumer: expect.any(Object), connect: expect.any(Function), - context: { - Provider: expect.any(Object), - Consumer: expect.any(Object), - }, + context: expect.any(Object), }); }); test(' passes state to ', () => { - const store = createStore({ hello: 'world' }); - const { Provider, Consumer } = createContext(store); + const { store } = create({ hello: 'world' }); + const { Provider, Consumer } = createStateContainerReactHelpers(); ReactDOM.render( - - {({ hello }) => hello} + + {(s: typeof store) => s.get().hello} , container ); @@ -74,8 +79,8 @@ interface Props1 { } test(' passes state to connect()()', () => { - const store = createStore({ hello: 'Bob' }); - const { Provider, connect } = createContext(store); + const { store } = create({ hello: 'Bob' }); + const { Provider, connect } = createStateContainerReactHelpers(); const Demo: React.FC = ({ message, stop }) => ( <> @@ -87,7 +92,7 @@ test(' passes state to connect()()', () => { const DemoConnected = connect(mergeProps)(Demo); ReactDOM.render( - + , container @@ -97,13 +102,13 @@ test(' passes state to connect()()', () => { }); test('context receives Redux store', () => { - const store = createStore({ foo: 'bar' }); - const { Provider, context } = createContext(store); + const { store } = create({ foo: 'bar' }); + const { Provider, context } = createStateContainerReactHelpers(); ReactDOM.render( /* eslint-disable no-shadow */ - - {({ store }) => store.getState().foo} + + {store => store.get().foo} , /* eslint-enable no-shadow */ container @@ -117,16 +122,16 @@ xtest('can use multiple stores in one React app', () => {}); describe('hooks', () => { describe('useStore', () => { test('can select store using useStore hook', () => { - const store = createStore({ foo: 'bar' }); - const { Provider, useStore } = createContext(store); + const { store } = create({ foo: 'bar' }); + const { Provider, useContainer } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { // eslint-disable-next-line no-shadow - const store = useStore(); + const store = useContainer(); return <>{store.get().foo}; }; ReactDOM.render( - + , container @@ -138,15 +143,15 @@ describe('hooks', () => { describe('useState', () => { test('can select state using useState hook', () => { - const store = createStore({ foo: 'qux' }); - const { Provider, useState } = createContext(store); + const { store } = create({ foo: 'qux' }); + const { Provider, useState } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const { foo } = useState(); return <>{foo}; }; ReactDOM.render( - + , container @@ -156,18 +161,23 @@ describe('hooks', () => { }); test('re-renders when state changes', () => { - const store = createStore({ foo: 'bar' }); - const { setFoo } = store.createMutators({ - setFoo: state => foo => ({ ...state, foo }), - }); - const { Provider, useState } = createContext(store); + const { + store, + mutators: { setFoo }, + } = create( + { foo: 'bar' }, + { + setFoo: (state: { foo: string }) => (foo: string) => ({ ...state, foo }), + } + ); + const { Provider, useState } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const { foo } = useState(); return <>{foo}; }; ReactDOM.render( - + , container @@ -181,26 +191,31 @@ describe('hooks', () => { }); }); - describe('useMutations', () => { - test('useMutations hook returns mutations that can update state', () => { - const store = createStore< + describe('useTransitions', () => { + test('useTransitions hook returns mutations that can update state', () => { + const { store } = create< { cnt: number; }, + any + >( { - increment: (value: number) => void; + cnt: 0, + }, + { + increment: (state: { cnt: number }) => (value: number) => ({ + ...state, + cnt: state.cnt + value, + }), } - >({ - cnt: 0, - }); - store.createMutators({ - increment: state => value => ({ ...state, cnt: state.cnt + value }), - }); + ); - const { Provider, useState, useMutators } = createContext(store); + const { Provider, useState, useTransitions } = createStateContainerReactHelpers< + typeof store + >(); const Demo: React.FC<{}> = () => { const { cnt } = useState(); - const { increment } = useMutators(); + const { increment } = useTransitions(); return ( <> {cnt} @@ -210,7 +225,7 @@ describe('hooks', () => { }; ReactDOM.render( - + , container @@ -230,7 +245,7 @@ describe('hooks', () => { describe('useSelector', () => { test('can select deeply nested value', () => { - const store = createStore({ + const { store } = create({ foo: { bar: { baz: 'qux', @@ -238,14 +253,14 @@ describe('hooks', () => { }, }); const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const value = useSelector(selector); return <>{value}; }; ReactDOM.render( - + , container @@ -255,7 +270,7 @@ describe('hooks', () => { }); test('re-renders when state changes', () => { - const store = createStore({ + const { store, mutators } = create({ foo: { bar: { baz: 'qux', @@ -263,14 +278,14 @@ describe('hooks', () => { }, }); const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const value = useSelector(selector); return <>{value}; }; ReactDOM.render( - + , container @@ -278,7 +293,7 @@ describe('hooks', () => { expect(container!.innerHTML).toBe('qux'); act(() => { - store.set({ + mutators.set({ foo: { bar: { baz: 'quux', @@ -290,9 +305,9 @@ describe('hooks', () => { }); test("re-renders only when selector's result changes", async () => { - const store = createStore({ a: 'b', foo: 'bar' }); + const { store, mutators } = create({ a: 'b', foo: 'bar' }); const selector = (state: { foo: string }) => state.foo; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); let cnt = 0; const Demo: React.FC<{}> = () => { @@ -301,7 +316,7 @@ describe('hooks', () => { return <>{value}; }; ReactDOM.render( - + , container @@ -311,24 +326,24 @@ describe('hooks', () => { expect(cnt).toBe(1); act(() => { - store.set({ a: 'c', foo: 'bar' }); + mutators.set({ a: 'c', foo: 'bar' }); }); await new Promise(r => setTimeout(r, 1)); expect(cnt).toBe(1); act(() => { - store.set({ a: 'd', foo: 'bar 2' }); + mutators.set({ a: 'd', foo: 'bar 2' }); }); await new Promise(r => setTimeout(r, 1)); expect(cnt).toBe(2); }); - test('re-renders on same shape object', async () => { - const store = createStore({ foo: { bar: 'baz' } }); + test('does not re-render on same shape object', async () => { + const { store, mutators } = create({ foo: { bar: 'baz' } }); const selector = (state: { foo: any }) => state.foo; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); let cnt = 0; const Demo: React.FC<{}> = () => { @@ -337,7 +352,7 @@ describe('hooks', () => { return <>{JSON.stringify(value)}; }; ReactDOM.render( - + , container @@ -347,7 +362,14 @@ describe('hooks', () => { expect(cnt).toBe(1); act(() => { - store.set({ foo: { bar: 'baz' } }); + mutators.set({ foo: { bar: 'baz' } }); + }); + + await new Promise(r => setTimeout(r, 1)); + expect(cnt).toBe(1); + + act(() => { + mutators.set({ foo: { bar: 'qux' } }); }); await new Promise(r => setTimeout(r, 1)); @@ -355,10 +377,15 @@ describe('hooks', () => { }); test('can set custom comparator function to prevent re-renders on deep equality', async () => { - const store = createStore({ foo: { bar: 'baz' } }); + const { store, mutators } = create( + { foo: { bar: 'baz' } }, + { + set: () => (newState: { foo: { bar: string } }) => newState, + } + ); const selector = (state: { foo: any }) => state.foo; const comparator = (prev: any, curr: any) => JSON.stringify(prev) === JSON.stringify(curr); - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); let cnt = 0; const Demo: React.FC<{}> = () => { @@ -367,7 +394,7 @@ describe('hooks', () => { return <>{JSON.stringify(value)}; }; ReactDOM.render( - + , container @@ -377,7 +404,7 @@ describe('hooks', () => { expect(cnt).toBe(1); act(() => { - store.set({ foo: { bar: 'baz' } }); + mutators.set({ foo: { bar: 'baz' } }); }); await new Promise(r => setTimeout(r, 1)); diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts new file mode 100644 index 0000000000000..e94165cc48376 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts @@ -0,0 +1,77 @@ +/* + * 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 * as React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import defaultComparator from 'fast-deep-equal'; +import { Comparator, Connect, StateContainer, UnboxState } from './types'; + +const { useContext, useLayoutEffect, useRef, createElement: h } = React; + +export const createStateContainerReactHelpers = >() => { + const context = React.createContext(null as any); + + const useContainer = (): Container => useContext(context); + + const useState = (): UnboxState => { + const { state$, get } = useContainer(); + const value = useObservable(state$, get()); + return value; + }; + + const useTransitions = () => useContainer().transitions; + + const useSelector = ( + selector: (state: UnboxState) => Result, + comparator: Comparator = defaultComparator + ): Result => { + const { state$, get } = useContainer(); + const lastValueRef = useRef(get()); + const [value, setValue] = React.useState(() => { + const newValue = selector(get()); + lastValueRef.current = newValue; + return newValue; + }); + useLayoutEffect(() => { + const subscription = state$.subscribe((currentState: UnboxState) => { + const newValue = selector(currentState); + if (!comparator(lastValueRef.current, newValue)) { + lastValueRef.current = newValue; + setValue(newValue); + } + }); + return () => subscription.unsubscribe(); + }, [state$, comparator]); + return value; + }; + + const connect: Connect> = mapStateToProp => component => props => + h(component, { ...useSelector(mapStateToProp), ...props } as any); + + return { + Provider: context.Provider, + Consumer: context.Consumer, + context, + useContainer, + useState, + useTransitions, + useSelector, + connect, + }; +}; diff --git a/src/plugins/kibana_utils/public/store/index.ts b/src/plugins/kibana_utils/public/state_containers/index.ts similarity index 86% rename from src/plugins/kibana_utils/public/store/index.ts rename to src/plugins/kibana_utils/public/state_containers/index.ts index 468e8ab8c5ade..43e204ecb79f7 100644 --- a/src/plugins/kibana_utils/public/store/index.ts +++ b/src/plugins/kibana_utils/public/state_containers/index.ts @@ -17,5 +17,6 @@ * under the License. */ -export * from './create_store'; -export * from './react'; +export * from './types'; +export * from './create_state_container'; +export * from './create_state_container_react_helpers'; diff --git a/src/plugins/kibana_utils/public/state_containers/types.ts b/src/plugins/kibana_utils/public/state_containers/types.ts new file mode 100644 index 0000000000000..e0a1a18972635 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/types.ts @@ -0,0 +1,99 @@ +/* + * 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 { Observable } from 'rxjs'; +import { Ensure, RecursiveReadonly } from '@kbn/utility-types'; + +export interface TransitionDescription { + type: Type; + args: Args; +} +export type Transition = (...args: Args) => State; +export type PureTransition = ( + state: RecursiveReadonly +) => Transition; +export type EnsurePureTransition = Ensure>; +export type PureTransitionToTransition> = ReturnType; +export type PureTransitionsToTransitions = { + [K in keyof T]: PureTransitionToTransition>; +}; + +export interface BaseStateContainer { + get: () => RecursiveReadonly; + set: (state: State) => void; + state$: Observable>; +} + +export interface StateContainer< + State, + PureTransitions extends object, + PureSelectors extends object = {} +> extends BaseStateContainer { + transitions: Readonly>; + selectors: Readonly>; +} + +export interface ReduxLikeStateContainer< + State, + PureTransitions extends object, + PureSelectors extends object = {} +> extends StateContainer { + getState: () => RecursiveReadonly; + reducer: Reducer>; + replaceReducer: (nextReducer: Reducer>) => void; + dispatch: (action: TransitionDescription) => void; + addMiddleware: (middleware: Middleware>) => void; + subscribe: (listener: (state: RecursiveReadonly) => void) => () => void; +} + +export type Dispatch = (action: T) => void; + +export type Middleware = ( + store: Pick, 'getState' | 'dispatch'> +) => ( + next: (action: TransitionDescription) => TransitionDescription | any +) => Dispatch; + +export type Reducer = (state: State, action: TransitionDescription) => State; + +export type UnboxState< + Container extends StateContainer +> = Container extends StateContainer ? T : never; +export type UnboxTransitions< + Container extends StateContainer +> = Container extends StateContainer ? T : never; + +export type Selector = (...args: Args) => Result; +export type PureSelector = ( + state: State +) => Selector; +export type EnsurePureSelector = Ensure>; +export type PureSelectorToSelector> = ReturnType< + EnsurePureSelector +>; +export type PureSelectorsToSelectors = { + [K in keyof T]: PureSelectorToSelector>; +}; + +export type Comparator = (previous: Result, current: Result) => boolean; + +export type MapStateToProps = (state: State) => StateProps; +export type Connect = ( + mapStateToProp: MapStateToProps> +) => (component: React.ComponentType) => React.FC>; diff --git a/src/plugins/kibana_utils/public/store/create_store.test.ts b/src/plugins/kibana_utils/public/store/create_store.test.ts deleted file mode 100644 index cfdeb76254003..0000000000000 --- a/src/plugins/kibana_utils/public/store/create_store.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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 { createStore } from './create_store'; - -test('can create store', () => { - const store = createStore({}); - expect(store).toMatchObject({ - get: expect.any(Function), - set: expect.any(Function), - state$: expect.any(Object), - createMutators: expect.any(Function), - mutators: expect.any(Object), - redux: { - getState: expect.any(Function), - dispatch: expect.any(Function), - subscribe: expect.any(Function), - }, - }); -}); - -test('can set default state', () => { - const defaultState = { - foo: 'bar', - }; - const store = createStore(defaultState); - expect(store.get()).toEqual(defaultState); - expect(store.redux.getState()).toEqual(defaultState); -}); - -test('can set state', () => { - const defaultState = { - foo: 'bar', - }; - const newState = { - foo: 'baz', - }; - const store = createStore(defaultState); - - store.set(newState); - - expect(store.get()).toEqual(newState); - expect(store.redux.getState()).toEqual(newState); -}); - -test('does not shallow merge states', () => { - const defaultState = { - foo: 'bar', - }; - const newState = { - foo2: 'baz', - }; - const store = createStore(defaultState); - - store.set(newState); - - expect(store.get()).toEqual(newState); - expect(store.redux.getState()).toEqual(newState); -}); - -test('can subscribe and unsubscribe to state changes', () => { - const store = createStore({}); - const spy = jest.fn(); - const subscription = store.state$.subscribe(spy); - store.set({ a: 1 }); - store.set({ a: 2 }); - subscription.unsubscribe(); - store.set({ a: 3 }); - - expect(spy).toHaveBeenCalledTimes(2); - expect(spy.mock.calls[0][0]).toEqual({ a: 1 }); - expect(spy.mock.calls[1][0]).toEqual({ a: 2 }); -}); - -test('multiple subscribers can subscribe', () => { - const store = createStore({}); - const spy1 = jest.fn(); - const spy2 = jest.fn(); - const subscription1 = store.state$.subscribe(spy1); - const subscription2 = store.state$.subscribe(spy2); - store.set({ a: 1 }); - subscription1.unsubscribe(); - store.set({ a: 2 }); - subscription2.unsubscribe(); - store.set({ a: 3 }); - - expect(spy1).toHaveBeenCalledTimes(1); - expect(spy2).toHaveBeenCalledTimes(2); - expect(spy1.mock.calls[0][0]).toEqual({ a: 1 }); - expect(spy2.mock.calls[0][0]).toEqual({ a: 1 }); - expect(spy2.mock.calls[1][0]).toEqual({ a: 2 }); -}); - -test('creates impure mutators from pure mutators', () => { - const store = createStore({}); - const mutators = store.createMutators({ - setFoo: _ => bar => ({ foo: bar }), - }); - - expect(typeof mutators.setFoo).toBe('function'); -}); - -test('mutators can update state', () => { - const store = createStore({ - value: 0, - foo: 'bar', - }); - const mutators = store.createMutators({ - add: state => increment => ({ ...state, value: state.value + increment }), - setFoo: state => bar => ({ ...state, foo: bar }), - }); - - expect(store.get()).toEqual({ - value: 0, - foo: 'bar', - }); - - mutators.add(11); - mutators.setFoo('baz'); - - expect(store.get()).toEqual({ - value: 11, - foo: 'baz', - }); - - mutators.add(-20); - mutators.setFoo('bazooka'); - - expect(store.get()).toEqual({ - value: -9, - foo: 'bazooka', - }); -}); - -test('mutators methods are not bound', () => { - const store = createStore({ value: -3 }); - const { add } = store.createMutators({ - add: state => increment => ({ ...state, value: state.value + increment }), - }); - - expect(store.get()).toEqual({ value: -3 }); - add(4); - expect(store.get()).toEqual({ value: 1 }); -}); - -test('created mutators are saved in store object', () => { - const store = createStore< - any, - { - add: (increment: number) => void; - } - >({ value: -3 }); - - store.createMutators({ - add: state => increment => ({ ...state, value: state.value + increment }), - }); - - expect(typeof store.mutators.add).toBe('function'); - store.mutators.add(5); - expect(store.get()).toEqual({ value: 2 }); -}); diff --git a/src/plugins/kibana_utils/public/store/create_store.ts b/src/plugins/kibana_utils/public/store/create_store.ts deleted file mode 100644 index 315523360f92d..0000000000000 --- a/src/plugins/kibana_utils/public/store/create_store.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 { createStore as createReduxStore, Reducer } from 'redux'; -import { Subject, Observable } from 'rxjs'; -import { AppStore, Mutators, PureMutators } from './types'; - -const SET = '__SET__'; - -export const createStore = < - State extends {}, - StateMutators extends Mutators> = {} ->( - defaultState: State -): AppStore => { - const pureMutators: PureMutators = {}; - const mutators: StateMutators = {} as StateMutators; - const reducer: Reducer = (state, action) => { - const pureMutator = pureMutators[action.type]; - if (pureMutator) { - return pureMutator(state)(...action.args); - } - - switch (action.type) { - case SET: - return action.state; - default: - return state; - } - }; - const redux = createReduxStore(reducer, defaultState as any); - - const get = redux.getState; - - const set = (state: State) => - redux.dispatch({ - type: SET, - state, - }); - - const state$ = new Subject(); - redux.subscribe(() => { - state$.next(get()); - }); - - const createMutators: AppStore['createMutators'] = newPureMutators => { - const result: Mutators = {}; - for (const type of Object.keys(newPureMutators)) { - result[type] = (...args) => { - redux.dispatch({ - type, - args, - }); - }; - } - Object.assign(pureMutators, newPureMutators); - Object.assign(mutators, result); - return result; - }; - - return { - get, - set, - redux, - state$: (state$ as unknown) as Observable, - createMutators, - mutators, - }; -}; diff --git a/src/plugins/kibana_utils/public/store/observable_selector.ts b/src/plugins/kibana_utils/public/store/observable_selector.ts deleted file mode 100644 index 6ba6f42296a6c..0000000000000 --- a/src/plugins/kibana_utils/public/store/observable_selector.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { Observable, BehaviorSubject } from 'rxjs'; - -export type Selector = (state: State) => Result; -export type Comparator = (previous: Result, current: Result) => boolean; -export type Unsubscribe = () => void; - -const defaultComparator: Comparator = (previous, current) => previous === current; - -export const observableSelector = ( - state: State, - state$: Observable, - selector: Selector, - comparator: Comparator = defaultComparator -): [Observable, Unsubscribe] => { - let previousResult: Result = selector(state); - const result$ = new BehaviorSubject(previousResult); - - const subscription = state$.subscribe(value => { - const result = selector(value); - const isEqual: boolean = comparator(previousResult, result); - if (!isEqual) { - result$.next(result); - } - previousResult = result; - }); - - return [(result$ as unknown) as Observable, subscription.unsubscribe]; -}; diff --git a/src/plugins/kibana_utils/public/store/react.ts b/src/plugins/kibana_utils/public/store/react.ts deleted file mode 100644 index 00861b2b0b8fe..0000000000000 --- a/src/plugins/kibana_utils/public/store/react.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 * as React from 'react'; -import { Provider as ReactReduxProvider, connect as reactReduxConnect } from 'react-redux'; -import { Store } from 'redux'; -import { AppStore, Mutators, PureMutators } from './types'; -import { observableSelector, Selector, Comparator } from './observable_selector'; -// TODO: Below import is temporary, use `react-use` lib instead. -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { useObservable } from '../../../kibana_react/public/util/use_observable'; - -const { useMemo, useLayoutEffect, useContext, createElement, Fragment } = React; - -/** - * @note - * Types in `react-redux` seem to be quite off compared to reality - * that's why a lot of `any`s below. - */ - -export interface ConsumerProps { - children: (state: State) => React.ReactChild; -} - -export type MapStateToProps = (state: State) => StateProps; - -// TODO: `Omit` is generally part of TypeScript, but it currently does not exist in our build. -type Omit = Pick>; -export type Connect = ( - mapStateToProp: MapStateToProps> -) => (component: React.ComponentType) => React.FC>; - -interface ReduxContextValue { - store: Store; -} - -const mapDispatchToProps = () => ({}); -const mergeProps: any = (stateProps: any, dispatchProps: any, ownProps: any) => ({ - ...ownProps, - ...stateProps, - ...dispatchProps, -}); - -export const createContext = < - State extends {}, - StateMutators extends Mutators> = {} ->( - store: AppStore -) => { - const { redux } = store; - (redux as any).__appStore = store; - const context = React.createContext({ store: redux }); - - const useStore = (): AppStore => { - // eslint-disable-next-line no-shadow - const { store } = useContext(context); - return (store as any).__appStore; - }; - - const useState = (): State => { - const { state$, get } = useStore(); - const state = useObservable(state$, get()); - return state; - }; - - const useMutators = (): StateMutators => useStore().mutators; - - const useSelector = ( - selector: Selector, - comparator?: Comparator - ): Result => { - const { state$, get } = useStore(); - /* eslint-disable react-hooks/exhaustive-deps */ - const [observable$, unsubscribe] = useMemo( - () => observableSelector(get(), state$, selector, comparator), - [state$] - ); - /* eslint-enable react-hooks/exhaustive-deps */ - useLayoutEffect(() => unsubscribe, [observable$, unsubscribe]); - const value = useObservable(observable$, selector(get())); - return value; - }; - - const Provider: React.FC<{}> = ({ children }) => - createElement(ReactReduxProvider, { - store: redux, - context, - children, - } as any); - - const Consumer: React.FC> = ({ children }) => { - const state = useState(); - return createElement(Fragment, { children: children(state) }); - }; - - const options: any = { context }; - const connect: Connect = mapStateToProps => - reactReduxConnect(mapStateToProps, mapDispatchToProps, mergeProps, options) as any; - - return { - Provider, - Consumer, - connect, - context, - useStore, - useState, - useMutators, - useSelector, - }; -}; diff --git a/yarn.lock b/yarn.lock index b4960a6cd01e0..dcaaa3da9fd75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6809,6 +6809,11 @@ bounce@1.x.x: boom "7.x.x" hoek "5.x.x" +bowser@^1.7.3: + version "1.9.4" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" + integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== + boxen@^1.2.1, boxen@^1.2.2: version "1.3.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -8719,6 +8724,13 @@ copy-to-clipboard@^3.0.8: dependencies: toggle-selection "^1.0.3" +copy-to-clipboard@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467" + integrity sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w== + dependencies: + toggle-selection "^1.0.6" + copy-webpack-plugin@^5.0.4: version "5.0.4" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.0.4.tgz#c78126f604e24f194c6ec2f43a64e232b5d43655" @@ -9096,6 +9108,14 @@ css-color-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= +css-in-js-utils@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99" + integrity sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA== + dependencies: + hyphenate-style-name "^1.0.2" + isobject "^3.0.1" + css-loader@2.1.1, css-loader@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea" @@ -9181,6 +9201,14 @@ css-tree@1.0.0-alpha.29: mdn-data "~1.1.0" source-map "^0.5.3" +css-tree@^1.0.0-alpha.28: + version "1.0.0-alpha.39" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" + integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA== + dependencies: + mdn-data "2.0.6" + source-map "^0.6.1" + css-url-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec" @@ -9252,7 +9280,7 @@ cssstyle@^2.0.0: dependencies: cssom "~0.3.6" -csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7: +csstype@^2.2.0, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5" integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ== @@ -10998,6 +11026,13 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +error-stack-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.4.tgz#a757397dc5d9de973ac9a5d7d4e8ade7cfae9101" + integrity sha512-fZ0KkoxSjLFmhW5lHbUT3tLwy3nX1qEzMYo8koY1vrsAco53CMT1djnBSeC/wUjTEZRhZl9iRw7PaMaxfJ4wzQ== + dependencies: + stackframe "^1.1.0" + error@^7.0.0, error@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" @@ -12174,6 +12209,11 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + fast-diff@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" @@ -12240,6 +12280,11 @@ fast-stream-to-buffer@^1.0.0: dependencies: end-of-stream "^1.4.1" +fastest-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028" + integrity sha1-kSLUBtTJ2YvqZEpraFPVh0uHsCg= + fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" @@ -15158,6 +15203,11 @@ hyperlinker@^1.0.0: resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ== +hyphenate-style-name@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" + integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== + i18n-iso-countries@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-4.3.1.tgz#f110a8824ce14edbb0eb8f3b0bd817ff950af37c" @@ -15437,6 +15487,14 @@ ini@^1.2.0, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== +inline-style-prefixer@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz#d390957d26f281255fe101da863158ac6eb60911" + integrity sha512-N8nVhwfYga9MiV9jWlwfdj1UDIaZlBFu4cJSJkIr7tZX7sHpHhGR5su1qdpW+7KPL8ISTvCIkcaFi/JdBknvPg== + dependencies: + bowser "^1.7.3" + css-in-js-utils "^2.0.0" + inline-style@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/inline-style/-/inline-style-2.0.0.tgz#2fa9cf624596a8109355b925094e138bbd5ea29b" @@ -18958,6 +19016,11 @@ mdast-add-list-metadata@1.0.1: dependencies: unist-util-visit-parents "1.1.2" +mdn-data@2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" + integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== + mdn-data@~1.1.0: version "1.1.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" @@ -19828,6 +19891,20 @@ nan@^2.10.0, nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nano-css@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.2.1.tgz#73b8470fa40b028a134d3393ae36bbb34b9fa332" + integrity sha512-T54okxMAha0+de+W8o3qFtuWhTxYvqQh2ku1cYEqTTP9mR62nWV2lLK9qRuAGWmoaYWhU7K4evT9Lc1iF65wuw== + dependencies: + css-tree "^1.0.0-alpha.28" + csstype "^2.5.5" + fastest-stable-stringify "^1.0.1" + inline-style-prefixer "^4.0.0" + rtl-css-js "^1.9.0" + sourcemap-codec "^1.4.1" + stacktrace-js "^2.0.0" + stylis "3.5.0" + nano-time@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" @@ -23530,6 +23607,21 @@ react-transition-group@^2.2.1: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" +react-use@^13.10.2: + version "13.10.2" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.10.2.tgz#4250d258ca9068662943299c01794a136408c8e9" + integrity sha512-z3VFSiPHW6arViGVnajO7YKY5OD+Z9LWcImoJdYHkau23cLSoTctxM3XENLpGxjhJlHaYiQZ6pPgq7pwGTqSZA== + dependencies: + copy-to-clipboard "^3.2.0" + nano-css "^5.2.1" + react-fast-compare "^2.0.4" + resize-observer-polyfill "^1.5.1" + screenfull "^5.0.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^2.1.0" + ts-easing "^0.2.0" + tslib "^1.10.0" + react-virtualized@^9.18.5: version "9.20.1" resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.20.1.tgz#02dc08fe9070386b8c48e2ac56bce7af0208d22d" @@ -24892,6 +24984,13 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +rtl-css-js@^1.9.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.13.1.tgz#80deabf6e8f36d6767d495cd3eb60fecb20c67e1" + integrity sha512-jgkIDj6Xi25kAEm5oYM3ZMFiOQhpLEcXi2LY/6bVr91cVz73hciHKneL5AMVPxOcks/JuizSaaNsvNRkeAWe3w== + dependencies: + "@babel/runtime" "^7.1.2" + run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -25189,6 +25288,11 @@ scoped-regex@^1.0.0: resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8" integrity sha1-o0a7Gs1CB65wvXwMfKnlZra63bg= +screenfull@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.0.tgz#5c2010c0e84fd4157bf852877698f90b8cbe96f6" + integrity sha512-yShzhaIoE9OtOhWVyBBffA6V98CDCoyHTsp8228blmqYy1Z5bddzE/4FPiJKlr8DVR4VBiiUyfPzIQPIYDkeMA== + script-loader@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/script-loader/-/script-loader-0.7.2.tgz#2016db6f86f25f5cf56da38915d83378bb166ba7" @@ -25421,6 +25525,11 @@ set-getter@^0.1.0: dependencies: to-object-path "^0.3.0" +set-harmonic-interval@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249" + integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g== + set-immediate-shim@^1.0.0, set-immediate-shim@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" @@ -25897,6 +26006,11 @@ source-map@0.1.32: dependencies: amdefine ">=0.0.4" +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= + "source-map@>= 0.1.2": version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" @@ -25933,6 +26047,11 @@ source-map@~0.2.0: dependencies: amdefine ">=0.0.4" +sourcemap-codec@^1.4.1: + version "1.4.6" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9" + integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg== + space-separated-tokens@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412" @@ -26134,6 +26253,13 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stack-generator@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.4.tgz#027513eab2b195bbb43b9c8360ba2dd0ab54de09" + integrity sha512-ha1gosTNcgxwzo9uKTQ8zZ49aUp5FIUW58YHFxCqaAHtE0XqBg0chGFYA1MfmW//x1KWq3F4G7Ug7bJh4RiRtg== + dependencies: + stackframe "^1.1.0" + stack-trace@0.0.10, stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -26144,6 +26270,11 @@ stack-utils@^1.0.1: resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" integrity sha1-1PM6tU6OOHeLDKXP07OvsS22hiA= +stackframe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.1.0.tgz#e3fc2eb912259479c9822f7d1f1ff365bd5cbc83" + integrity sha512-Vx6W1Yvy+AM1R/ckVwcHQHV147pTPBKWCRLrXMuPrFVfvBUc3os7PR1QLIWCMhPpRg5eX9ojzbQIMLGBwyLjqg== + stackman@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/stackman/-/stackman-4.0.0.tgz#3ccdc8682fee36373ed2492dc3dad546eb44647d" @@ -26155,6 +26286,23 @@ stackman@^4.0.0: error-callsites "^2.0.2" load-source-map "^1.0.0" +stacktrace-gps@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.3.tgz#b89f84cc13bb925b96607e737b617c8715facf57" + integrity sha512-51Rr7dXkyFUKNmhY/vqZWK+EvdsfFSRiQVtgHTFlAdNIYaDD7bVh21yBHXaNWAvTD+w+QSjxHg7/v6Tz4veExA== + dependencies: + source-map "0.5.6" + stackframe "^1.1.0" + +stacktrace-js@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.1.tgz#ebdb0e9a16e6f171f96ca7878404e7f15c3d42ba" + integrity sha512-13oDNgBSeWtdGa4/2BycNyKqe+VktCoJ8VLx4pDoJkwGGJVtiHdfMOAj3aW9xTi8oR2v34z9IcvfCvT6XNdNAw== + dependencies: + error-stack-parser "^2.0.4" + stack-generator "^2.0.4" + stacktrace-gps "^3.0.3" + state-toggle@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425" @@ -26677,6 +26825,11 @@ stylis-rule-sheet@^0.0.10: resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw== +stylis@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1" + integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw== + stylus-lookup@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-3.0.2.tgz#c9eca3ff799691020f30b382260a67355fefdddd" @@ -27520,7 +27673,7 @@ to-through@^2.0.0: dependencies: through2 "^2.0.3" -toggle-selection@^1.0.3: +toggle-selection@^1.0.3, toggle-selection@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= @@ -27666,6 +27819,11 @@ ts-dedent@^1.1.0: resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-1.1.0.tgz#67983940793183dc7c7f820acb66ba02cdc33c6e" integrity sha512-CVCvDwMBWZKjDxpN3mU/Dx1v3k+sJgE8nrhXcC9vRopRfoa7vVzilNvHEAUi5jQnmFHpnxDx5jZdI1TpG8ny2g== +ts-easing@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" + integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== + ts-invariant@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.2.1.tgz#3d587f9d6e3bded97bf9ec17951dd9814d5a9d3f" @@ -27721,7 +27879,7 @@ tsd@^0.7.4: typescript "^3.0.1" update-notifier "^2.5.0" -tslib@^1: +tslib@^1, tslib@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== From 6af9f9bea601af976859eae125d4f0a7b9602bdc Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 6 Dec 2019 19:55:57 +0100 Subject: [PATCH 17/35] update columns (#51892) --- .../__snapshots__/ping_list.test.tsx.snap | 6 +- .../__tests__/ping_list.test.tsx | 4 +- .../components/functional/ping_list/index.tsx | 7 +++ .../functional/{ => ping_list}/ping_list.tsx | 62 +++++++++---------- 4 files changed, 42 insertions(+), 37 deletions(-) rename x-pack/legacy/plugins/uptime/public/components/functional/{ => ping_list}/__tests__/__snapshots__/ping_list.test.tsx.snap (99%) rename x-pack/legacy/plugins/uptime/public/components/functional/{ => ping_list}/__tests__/ping_list.test.tsx (98%) create mode 100644 x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx rename x-pack/legacy/plugins/uptime/public/components/functional/{ => ping_list}/ping_list.tsx (88%) diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/ping_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap similarity index 99% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/ping_list.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap index 9c6d2e2f495c1..5f60ce38500c8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/ping_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap @@ -121,8 +121,7 @@ exports[`PingList component renders sorted list without errors 1`] = ` "render": [Function], }, Object { - "align": "left", - "dataType": "number", + "align": "center", "field": "observer.geo.name", "name": "Location", "render": [Function], @@ -140,9 +139,10 @@ exports[`PingList component renders sorted list without errors 1`] = ` "render": [Function], }, Object { - "align": "left", + "align": "right", "field": "error.type", "name": "Error type", + "render": [Function], }, Object { "align": "right", diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/ping_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/ping_list.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx index 46da7e5233354..43adc4da85f32 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/ping_list.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx @@ -6,10 +6,10 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { PingResults, Ping } from '../../../../common/graphql/types'; +import { PingResults, Ping } from '../../../../../common/graphql/types'; import { PingListComponent, AllLocationOption, toggleDetails } from '../ping_list'; import { EuiComboBoxOptionProps } from '@elastic/eui'; -import { ExpandedRowMap } from '../monitor_list/types'; +import { ExpandedRowMap } from '../../monitor_list/types'; describe('PingList component', () => { let pingList: { allPings: PingResults }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx new file mode 100644 index 0000000000000..e57b229dfd973 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx @@ -0,0 +1,7 @@ +/* + * 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 './ping_list'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx similarity index 88% rename from x-pack/legacy/plugins/uptime/public/components/functional/ping_list.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx index fb7c0a5af1e7f..0a97b596a7a71 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx @@ -24,13 +24,13 @@ import moment from 'moment'; import React, { Fragment, useEffect, useState } from 'react'; // @ts-ignore formatNumber import { formatNumber } from '@elastic/eui/lib/services/format'; -import { Ping, PingResults } from '../../../common/graphql/types'; -import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../lib/helper'; -import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order'; -import { pingsQuery } from '../../queries'; -import { LocationName } from './location_name'; -import { Criteria, Pagination } from './monitor_list'; -import { PingListExpandedRowComponent } from './ping_list/expanded_row'; +import { Ping, PingResults } from '../../../../common/graphql/types'; +import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper'; +import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../../higher_order'; +import { pingsQuery } from '../../../queries'; +import { LocationName } from './../location_name'; +import { Criteria, Pagination } from './../monitor_list'; +import { PingListExpandedRowComponent } from './expanded_row'; interface PingListQueryResult { allPings?: PingResults; @@ -83,6 +83,10 @@ export const PingListComponent = ({ }: Props) => { const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState({}); + useEffect(() => { + onUpdateApp(); + }, [selectedOption]); + const statusOptions = [ { text: i18n.translate('xpack.uptime.pingList.statusOptions.allStatusOptionLabel', { @@ -141,8 +145,7 @@ export const PingListComponent = ({ ), }, { - align: 'left', - dataType: 'number', + align: 'center', field: 'observer.geo.name', name: i18n.translate('xpack.uptime.pingList.locationNameColumnLabel', { defaultMessage: 'Location', @@ -170,36 +173,31 @@ export const PingListComponent = ({ }), }, { - align: 'left', + align: 'right', field: 'error.type', name: i18n.translate('xpack.uptime.pingList.errorTypeColumnLabel', { defaultMessage: 'Error type', }), + render: (error: string) => error ?? '-', }, ]; - useEffect(() => { - onUpdateApp(); - }, [selectedOption]); - let pings: Ping[] = []; - if (data && data.allPings && data.allPings.pings) { - pings = data.allPings.pings; - const hasStatus: boolean = pings.reduce( - (hasHttpStatus: boolean, currentPing: Ping) => - hasHttpStatus || !!get(currentPing, 'http.response.status_code'), - false - ); - if (hasStatus) { - columns.push({ - field: 'http.response.status_code', - // @ts-ignore "align" property missing on type definition for column type - align: 'right', - name: i18n.translate('xpack.uptime.pingList.responseCodeColumnLabel', { - defaultMessage: 'Response code', - }), - render: (statusCode: string) => {statusCode}, - }); - } + + const pings: Ping[] = data?.allPings?.pings ?? []; + + const hasStatus: boolean = pings.some( + (currentPing: Ping) => !!currentPing?.http?.response?.status_code + ); + if (hasStatus) { + columns.push({ + field: 'http.response.status_code', + align: 'right', + name: i18n.translate('xpack.uptime.pingList.responseCodeColumnLabel', { + defaultMessage: 'Response code', + }), + render: (statusCode: string) => {statusCode}, + }); } + columns.push({ align: 'right', width: '40px', From e17539c5da78e1567804218c6beb18048595cea2 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 6 Dec 2019 12:27:06 -0700 Subject: [PATCH 18/35] [ci/pipeline/reportFailures] when aborted, run with --no-github-update (#52355) --- vars/kibanaPipeline.groovy | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 5b3cd071316e6..77907a07addd1 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -262,10 +262,13 @@ def buildXpack() { } def runErrorReporter() { + def status = buildUtils.getBuildStatus() + def dryRun = status != "ABORTED" ? "" : "--no-github-update" + bash( """ source src/dev/ci_setup/setup_env.sh - node scripts/report_failed_tests + node scripts/report_failed_tests ${dryRun} """, "Report failed tests, if necessary" ) From c3ddb53c660139977673a8103effe7f9bdc42bda Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 6 Dec 2019 12:31:19 -0700 Subject: [PATCH 19/35] [SIEM] Adds support for specifying default filters to StatefulEventsViewer (#52413) ## Summary Finishes plumbing through the `defaultFilters` prop on the `StatefuleEventsViewer` component so that your view will always be constrained by a specified filter. Also adds an example of doing so to the current WIP `SignalsTable`. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [ ] ~This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~ - [ ] ~Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~ - [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~ - [ ] ~[Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios~ - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ ### For maintainers - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [ ] ~This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ --- .../public/components/events_viewer/index.tsx | 7 +-- ...default_headers.tsx => default_config.tsx} | 47 +++++++++++++++++++ .../signals/default_model.tsx | 13 ----- .../pages/detection_engine/signals/index.tsx | 8 +++- 4 files changed, 57 insertions(+), 18 deletions(-) rename x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/{default_headers.tsx => default_config.tsx} (67%) delete mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx index 613861a4c905c..21292e4ac3254 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx @@ -83,6 +83,7 @@ const StatefulEventsViewerComponent = React.memo( createTimeline, columns, dataProviders, + defaultFilters = [], defaultModel, defaultIndices, deleteEventQuery, @@ -158,7 +159,7 @@ const StatefulEventsViewerComponent = React.memo( id={id} dataProviders={dataProviders!} end={end} - filters={filters} + filters={[...filters, ...defaultFilters]} headerFilterGroup={headerFilterGroup} indexPattern={indexPatterns ?? { fields: [], title: '' }} isLive={isLive} @@ -201,7 +202,7 @@ const makeMapStateToProps = () => { const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); const getEvents = timelineSelectors.getEventsByIdSelector(); - const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { + const mapStateToProps = (state: State, { id, defaultFilters = [], defaultModel }: OwnProps) => { const input: inputsModel.InputsRange = getInputsTimeline(state); const events: TimelineModel = getEvents(state, id) ?? defaultModel; const { columns, dataProviders, itemsPerPage, itemsPerPageOptions, kqlMode, sort } = events; @@ -209,7 +210,7 @@ const makeMapStateToProps = () => { return { columns, dataProviders, - filters: getGlobalFiltersQuerySelector(state), + filters: [...getGlobalFiltersQuerySelector(state), ...defaultFilters], id, isLive: input.policy.kind === 'interval', itemsPerPage, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_headers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx similarity index 67% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_headers.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx index d6bfcd80b9956..e90487a3b023c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_headers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx @@ -12,6 +12,48 @@ import { } from '../../../components/timeline/body/helpers'; import * as i18n from './translations'; +import { SubsetTimelineModel, timelineDefaults } from '../../../store/timeline/model'; +import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query'; + +export const signalsOpenFilters: esFilters.Filter[] = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.status', + params: { + query: 'open', + }, + }, + query: { + match_phrase: { + 'signal.status': 'open', + }, + }, + }, +]; + +export const signalsClosedFilters: esFilters.Filter[] = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.status', + params: { + query: 'closed', + }, + }, + query: { + match_phrase: { + 'signal.status': 'closed', + }, + }, + }, +]; export const signalsHeaders: ColumnHeader[] = [ { @@ -77,3 +119,8 @@ export const signalsHeaders: ColumnHeader[] = [ width: DEFAULT_DATE_COLUMN_MIN_WIDTH, }, ]; + +export const signalsDefaultModel: SubsetTimelineModel = { + ...timelineDefaults, + columns: signalsHeaders, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx deleted file mode 100644 index bb1f806d67c03..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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 { signalsHeaders } from './default_headers'; -import { SubsetTimelineModel, timelineDefaults } from '../../../store/timeline/model'; - -export const signalsDefaultModel: SubsetTimelineModel = { - ...timelineDefaults, - columns: signalsHeaders, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx index edc7ed133d10c..ca178db9cd97f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx @@ -12,7 +12,7 @@ import { GlobalTime } from '../../../containers/global_time'; import { StatefulEventsViewer } from '../../../components/events_viewer'; import * as i18n from './translations'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; -import { signalsDefaultModel } from './default_model'; +import { signalsClosedFilters, signalsDefaultModel, signalsOpenFilters } from './default_config'; const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; const FILTER_OPEN = 'open'; @@ -37,7 +37,10 @@ export const SignalsTableFilterGroup = React.memo( setFilterGroup(FILTER_CLOSED)} + onClick={() => { + setFilterGroup(FILTER_CLOSED); + onFilterGroupChanged(FILTER_CLOSED); + }} > {'Closed signals'} @@ -62,6 +65,7 @@ export const SignalsTable = React.memo(() => { {({ to, from, setQuery, deleteQuery, isInitializing }) => ( Date: Fri, 6 Dec 2019 14:55:16 -0500 Subject: [PATCH 20/35] Add Endpoint plugin and Resolver embeddable (#51994) * Add functional tests for plugins to x-pack (so we can do a functional test of the Resolver embeddable) * Add Endpoint plugin * Add Resolver embeddable * Test that Resolver embeddable can be rendered --- x-pack/.i18nrc.json | 9 +-- x-pack/plugins/endpoint/kibana.json | 9 +++ .../embeddables/resolver/embeddable.tsx | 34 +++++++++ .../public/embeddables/resolver/factory.ts | 31 ++++++++ .../public/embeddables/resolver/index.ts | 8 +++ x-pack/plugins/endpoint/public/index.ts | 21 ++++++ x-pack/plugins/endpoint/public/plugin.ts | 38 ++++++++++ x-pack/plugins/endpoint/server/index.ts | 26 +++++++ x-pack/plugins/endpoint/server/plugin.ts | 30 ++++++++ .../plugins/endpoint/server/routes/index.ts | 24 +++++++ x-pack/scripts/functional_tests.js | 1 + .../api_integration/apis/endpoint/index.ts | 13 ++++ .../api_integration/apis/endpoint/resolver.ts | 29 ++++++++ x-pack/test/api_integration/apis/index.js | 1 + x-pack/test/api_integration/config.js | 1 + x-pack/test/plugin_functional/config.ts | 72 +++++++++++++++++++ .../ftr_provider_context.d.ts | 11 +++ x-pack/test/plugin_functional/page_objects.ts | 6 ++ .../plugins/resolver_test/kibana.json | 9 +++ .../applications/resolver_test/index.tsx | 63 ++++++++++++++++ .../plugins/resolver_test/public/index.ts | 10 +++ .../plugins/resolver_test/public/plugin.ts | 53 ++++++++++++++ x-pack/test/plugin_functional/services.ts | 7 ++ .../test_suites/resolver/index.ts | 27 +++++++ 24 files changed, 529 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/endpoint/kibana.json create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/index.ts create mode 100644 x-pack/plugins/endpoint/public/index.ts create mode 100644 x-pack/plugins/endpoint/public/plugin.ts create mode 100644 x-pack/plugins/endpoint/server/index.ts create mode 100644 x-pack/plugins/endpoint/server/plugin.ts create mode 100644 x-pack/plugins/endpoint/server/routes/index.ts create mode 100644 x-pack/test/api_integration/apis/endpoint/index.ts create mode 100644 x-pack/test/api_integration/apis/endpoint/resolver.ts create mode 100644 x-pack/test/plugin_functional/config.ts create mode 100644 x-pack/test/plugin_functional/ftr_provider_context.d.ts create mode 100644 x-pack/test/plugin_functional/page_objects.ts create mode 100644 x-pack/test/plugin_functional/plugins/resolver_test/kibana.json create mode 100644 x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx create mode 100644 x-pack/test/plugin_functional/plugins/resolver_test/public/index.ts create mode 100644 x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts create mode 100644 x-pack/test/plugin_functional/services.ts create mode 100644 x-pack/test/plugin_functional/test_suites/resolver/index.ts diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 6d0da2f0b693d..180aafe504c63 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -9,6 +9,7 @@ "xpack.canvas": "legacy/plugins/canvas", "xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication", "xpack.dashboardMode": "legacy/plugins/dashboard_mode", + "xpack.endpoint": "plugins/endpoint", "xpack.features": "plugins/features", "xpack.fileUpload": "legacy/plugins/file_upload", "xpack.graph": "legacy/plugins/graph", @@ -18,20 +19,20 @@ "xpack.infra": "legacy/plugins/infra", "xpack.kueryAutocomplete": "legacy/plugins/kuery_autocomplete", "xpack.lens": "legacy/plugins/lens", - "xpack.licensing": "plugins/licensing", "xpack.licenseMgmt": "legacy/plugins/license_management", - "xpack.maps": "legacy/plugins/maps", - "xpack.ml": "legacy/plugins/ml", + "xpack.licensing": "plugins/licensing", "xpack.logstash": "legacy/plugins/logstash", "xpack.main": "legacy/plugins/xpack_main", + "xpack.maps": "legacy/plugins/maps", + "xpack.ml": "legacy/plugins/ml", "xpack.monitoring": "legacy/plugins/monitoring", "xpack.remoteClusters": "legacy/plugins/remote_clusters", "xpack.reporting": [ "plugins/reporting", "legacy/plugins/reporting" ], "xpack.rollupJobs": "legacy/plugins/rollup", "xpack.searchProfiler": "legacy/plugins/searchprofiler", - "xpack.siem": "legacy/plugins/siem", "xpack.security": ["legacy/plugins/security", "plugins/security"], "xpack.server": "legacy/server", + "xpack.siem": "legacy/plugins/siem", "xpack.snapshotRestore": "legacy/plugins/snapshot_restore", "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], "xpack.taskManager": "legacy/plugins/task_manager", diff --git a/x-pack/plugins/endpoint/kibana.json b/x-pack/plugins/endpoint/kibana.json new file mode 100644 index 0000000000000..a7fd20b93f62d --- /dev/null +++ b/x-pack/plugins/endpoint/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "endpoint", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "endpoint"], + "requiredPlugins": ["embeddable"], + "server": true, + "ui": true +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx new file mode 100644 index 0000000000000..55f9fd52f4662 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -0,0 +1,34 @@ +/* + * 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 { + EmbeddableInput, + IContainer, + Embeddable, +} from '../../../../../../src/plugins/embeddable/public'; + +export class ResolverEmbeddable extends Embeddable { + public readonly type = 'resolver'; + constructor(initialInput: EmbeddableInput, parent?: IContainer) { + super( + // Input state is irrelevant to this embeddable, just pass it along. + initialInput, + // Initial output state - this embeddable does not do anything with output, so just + // pass along an empty object. + {}, + // Optional parent component, this embeddable can optionally be rendered inside a container. + parent + ); + } + + public render(node: HTMLElement) { + node.innerHTML = '
Welcome from Resolver
'; + } + + public reload(): void { + throw new Error('Method not implemented.'); + } +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts new file mode 100644 index 0000000000000..aef2e309254ef --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts @@ -0,0 +1,31 @@ +/* + * 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 { ResolverEmbeddable } from './'; +import { + EmbeddableFactory, + EmbeddableInput, + IContainer, +} from '../../../../../../src/plugins/embeddable/public'; + +export class ResolverEmbeddableFactory extends EmbeddableFactory { + public readonly type = 'resolver'; + + public isEditable() { + return true; + } + + public async create(initialInput: EmbeddableInput, parent?: IContainer) { + return new ResolverEmbeddable(initialInput, parent); + } + + public getDisplayName() { + return i18n.translate('xpack.endpoint.resolver.displayNameTitle', { + defaultMessage: 'Resolver', + }); + } +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/index.ts new file mode 100644 index 0000000000000..e4f3cc90ae30a --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/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 { ResolverEmbeddableFactory } from './factory'; +export { ResolverEmbeddable } from './embeddable'; diff --git a/x-pack/plugins/endpoint/public/index.ts b/x-pack/plugins/endpoint/public/index.ts new file mode 100644 index 0000000000000..e6a7683efd9a3 --- /dev/null +++ b/x-pack/plugins/endpoint/public/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { PluginInitializer } from 'kibana/public'; +import { + EndpointPlugin, + EndpointPluginStart, + EndpointPluginSetup, + EndpointPluginStartDependencies, + EndpointPluginSetupDependencies, +} from './plugin'; + +export const plugin: PluginInitializer< + EndpointPluginSetup, + EndpointPluginStart, + EndpointPluginSetupDependencies, + EndpointPluginStartDependencies +> = () => new EndpointPlugin(); diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts new file mode 100644 index 0000000000000..21bf1b3cdea12 --- /dev/null +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -0,0 +1,38 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/public'; +import { IEmbeddableSetup } from 'src/plugins/embeddable/public'; +import { ResolverEmbeddableFactory } from './embeddables/resolver'; + +export type EndpointPluginStart = void; +export type EndpointPluginSetup = void; +export interface EndpointPluginSetupDependencies { + embeddable: IEmbeddableSetup; +} + +export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface + +export class EndpointPlugin + implements + Plugin< + EndpointPluginSetup, + EndpointPluginStart, + EndpointPluginSetupDependencies, + EndpointPluginStartDependencies + > { + public setup(_core: CoreSetup, plugins: EndpointPluginSetupDependencies) { + const resolverEmbeddableFactory = new ResolverEmbeddableFactory(); + plugins.embeddable.registerEmbeddableFactory( + resolverEmbeddableFactory.type, + resolverEmbeddableFactory + ); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/endpoint/server/index.ts b/x-pack/plugins/endpoint/server/index.ts new file mode 100644 index 0000000000000..f10bc7ee51b2c --- /dev/null +++ b/x-pack/plugins/endpoint/server/index.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 { schema } from '@kbn/config-schema'; +import { PluginInitializer } from 'src/core/server'; +import { + EndpointPlugin, + EndpointPluginStart, + EndpointPluginSetup, + EndpointPluginStartDependencies, + EndpointPluginSetupDependencies, +} from './plugin'; + +export const config = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), +}; + +export const plugin: PluginInitializer< + EndpointPluginStart, + EndpointPluginSetup, + EndpointPluginStartDependencies, + EndpointPluginSetupDependencies +> = () => new EndpointPlugin(); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts new file mode 100644 index 0000000000000..400b906c5230e --- /dev/null +++ b/x-pack/plugins/endpoint/server/plugin.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 { Plugin, CoreSetup } from 'kibana/server'; +import { addRoutes } from './routes'; + +export type EndpointPluginStart = void; +export type EndpointPluginSetup = void; +export interface EndpointPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface + +export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface + +export class EndpointPlugin + implements + Plugin< + EndpointPluginStart, + EndpointPluginSetup, + EndpointPluginStartDependencies, + EndpointPluginSetupDependencies + > { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + addRoutes(router); + } + + public start() {} +} diff --git a/x-pack/plugins/endpoint/server/routes/index.ts b/x-pack/plugins/endpoint/server/routes/index.ts new file mode 100644 index 0000000000000..517ee2a853660 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +export function addRoutes(router: IRouter) { + router.get( + { + path: '/api/endpoint/hello-world', + validate: false, + }, + async function greetingIndex(_context, _request, response) { + return response.ok({ + body: { hello: 'world' }, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + ); +} diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 2ac8fff6ef8ab..18ab9bad52450 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -15,6 +15,7 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), require.resolve('../test/plugin_api_integration/config.js'), + require.resolve('../test/plugin_functional/config'), require.resolve('../test/kerberos_api_integration/config'), require.resolve('../test/kerberos_api_integration/anonymous_access.config'), require.resolve('../test/saml_api_integration/config'), diff --git a/x-pack/test/api_integration/apis/endpoint/index.ts b/x-pack/test/api_integration/apis/endpoint/index.ts new file mode 100644 index 0000000000000..e0ffbb13e5978 --- /dev/null +++ b/x-pack/test/api_integration/apis/endpoint/index.ts @@ -0,0 +1,13 @@ +/* + * 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 default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('Endpoint plugin', function() { + loadTestFile(require.resolve('./resolver')); + }); +} diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts new file mode 100644 index 0000000000000..96d16e0d76e40 --- /dev/null +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -0,0 +1,29 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +const commonHeaders = { + Accept: 'application/json', + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('Resolver api', function() { + it('should respond to hello-world', async function() { + const { body } = await supertest + .get('/api/endpoint/hello-world') + .set(commonHeaders) + .expect('Content-Type', /application\/json/) + .expect(200); + + expect(body).to.eql({ hello: 'world' }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index ca339e9f407f2..ddf2c9a13ff67 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -28,5 +28,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./short_urls')); loadTestFile(require.resolve('./lens')); loadTestFile(require.resolve('./licensing')); + loadTestFile(require.resolve('./endpoint')); }); } diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 9c67dfe61b957..e5860fba80770 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -23,6 +23,7 @@ export async function getApiIntegrationConfig({ readConfigFile }) { ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), '--xpack.security.session.idleTimeout=3600000', // 1 hour '--optimize.enabled=false', + '--xpack.endpoint.enabled=true', ], }, esTestCluster: { diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts new file mode 100644 index 0000000000000..6c3c496da71f6 --- /dev/null +++ b/x-pack/test/plugin_functional/config.ts @@ -0,0 +1,72 @@ +/* + * 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 { resolve } from 'path'; +import fs from 'fs'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { services } from './services'; +import { pageObjects } from './page_objects'; + +// the default export of config files must be a config provider +// that returns an object with the projects config values + +/* eslint-disable import/no-default-export */ +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + // Find all folders in ./plugins since we treat all them as plugin folder + const allFiles = fs.readdirSync(resolve(__dirname, 'plugins')); + const plugins = allFiles.filter(file => + fs.statSync(resolve(__dirname, 'plugins', file)).isDirectory() + ); + + return { + // list paths to the files that contain your plugins tests + testFiles: [resolve(__dirname, './test_suites/resolver')], + + services, + pageObjects, + + servers: xpackFunctionalConfig.get('servers'), + + esTestCluster: xpackFunctionalConfig.get('esTestCluster'), + + kbnTestServer: { + ...xpackFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + ...plugins.map(pluginDir => `--plugin-path=${resolve(__dirname, 'plugins', pluginDir)}`), + // Required to load new platform plugins via `--plugin-path` flag. + '--env.name=development', + '--xpack.endpoint.enabled=true', + ], + }, + uiSettings: xpackFunctionalConfig.get('uiSettings'), + // the apps section defines the urls that + // `PageObjects.common.navigateTo(appKey)` will use. + // Merge urls for your plugin with the urls defined in + // Kibana's config in order to use this helper + apps: { + ...xpackFunctionalConfig.get('apps'), + resolverTest: { + pathname: '/app/resolver_test', + }, + }, + + // choose where esArchiver should load archives from + esArchiver: { + directory: resolve(__dirname, 'es_archives'), + }, + + // choose where screenshots should be saved + screenshots: { + directory: resolve(__dirname, 'screenshots'), + }, + + junit: { + reportName: 'Chrome X-Pack UI Plugin Functional Tests', + }, + }; +} diff --git a/x-pack/test/plugin_functional/ftr_provider_context.d.ts b/x-pack/test/plugin_functional/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..271f313d4bda9 --- /dev/null +++ b/x-pack/test/plugin_functional/ftr_provider_context.d.ts @@ -0,0 +1,11 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { services } from './services'; +import { pageObjects } from './page_objects'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/plugin_functional/page_objects.ts b/x-pack/test/plugin_functional/page_objects.ts new file mode 100644 index 0000000000000..a216b0f2cd47a --- /dev/null +++ b/x-pack/test/plugin_functional/page_objects.ts @@ -0,0 +1,6 @@ +/* + * 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 { pageObjects } from '../functional/page_objects'; diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json b/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json new file mode 100644 index 0000000000000..c715a0aaa3b20 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "resolver_test", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "resolver_test"], + "requiredPlugins": ["embeddable"], + "server": false, + "ui": true +} diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx new file mode 100644 index 0000000000000..98baad6a18411 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -0,0 +1,63 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { AppMountParameters } from 'kibana/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { IEmbeddable } from 'src/plugins/embeddable/public'; +import { useEffect } from 'react'; + +/** + * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. + */ +export function renderApp( + { element }: AppMountParameters, + embeddable: Promise +) { + ReactDOM.render( + + + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +} + +const AppRoot = React.memo( + ({ embeddable: embeddablePromise }: { embeddable: Promise }) => { + const [embeddable, setEmbeddable] = React.useState(undefined); + const [renderTarget, setRenderTarget] = React.useState(null); + + useEffect(() => { + let cleanUp; + Promise.race([ + new Promise((_resolve, reject) => { + cleanUp = reject; + }), + embeddablePromise, + ]).then(value => { + setEmbeddable(value); + }); + + return cleanUp; + }, [embeddablePromise]); + + useEffect(() => { + if (embeddable && renderTarget) { + embeddable.render(renderTarget); + return () => { + embeddable.destroy(); + }; + } + }, [embeddable, renderTarget]); + + return
; + } +); diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/index.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/index.ts new file mode 100644 index 0000000000000..c5f3c0e19138f --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { ResolverTestPlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new ResolverTestPlugin(); diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts new file mode 100644 index 0000000000000..f063271f4b5dd --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts @@ -0,0 +1,53 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { IEmbeddable, IEmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; + +export type ResolverTestPluginSetup = void; +export type ResolverTestPluginStart = void; +export interface ResolverTestPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface +export interface ResolverTestPluginStartDependencies { + embeddable: IEmbeddableStart; +} + +export class ResolverTestPlugin + implements + Plugin< + ResolverTestPluginSetup, + ResolverTestPluginStart, + ResolverTestPluginSetupDependencies, + ResolverTestPluginStartDependencies + > { + private resolveEmbeddable!: ( + value: IEmbeddable | undefined | PromiseLike | undefined + ) => void; + private embeddablePromise: Promise = new Promise< + IEmbeddable | undefined + >(resolve => { + this.resolveEmbeddable = resolve; + }); + public setup(core: CoreSetup) { + core.application.register({ + id: 'resolver_test', + title: i18n.translate('xpack.resolver_test.pluginTitle', { + defaultMessage: 'Resolver Test', + }), + mount: async (_context, params) => { + const { renderApp } = await import('./applications/resolver_test'); + return renderApp(params, this.embeddablePromise); + }, + }); + } + + public start(...args: [unknown, { embeddable: IEmbeddableStart }]) { + const [, plugins] = args; + const factory = plugins.embeddable.getEmbeddableFactory('resolver'); + this.resolveEmbeddable(factory.create({ id: 'test basic render' })); + } + public stop() {} +} diff --git a/x-pack/test/plugin_functional/services.ts b/x-pack/test/plugin_functional/services.ts new file mode 100644 index 0000000000000..5c807720b2867 --- /dev/null +++ b/x-pack/test/plugin_functional/services.ts @@ -0,0 +1,7 @@ +/* + * 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 { services } from '../functional/services'; diff --git a/x-pack/test/plugin_functional/test_suites/resolver/index.ts b/x-pack/test/plugin_functional/test_suites/resolver/index.ts new file mode 100644 index 0000000000000..a0735f216e309 --- /dev/null +++ b/x-pack/test/plugin_functional/test_suites/resolver/index.ts @@ -0,0 +1,27 @@ +/* + * 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 default function({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + describe('Resolver embeddable test app', function() { + this.tags('ciGroup7'); + + beforeEach(async function() { + await pageObjects.common.navigateToApp('resolverTest'); + }); + + it('renders a container div for the embeddable', async function() { + await testSubjects.existOrFail('resolverEmbeddableContainer'); + }); + it('renders resolver', async function() { + await testSubjects.existOrFail('resolverEmbeddable'); + }); + }); +} From f7f00819dc9d29b905107660173530e938ef901b Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 6 Dec 2019 16:17:18 -0500 Subject: [PATCH 21/35] Update default path linked on Kibana sidebar to avoid basename warning in browser. (#52008) --- x-pack/legacy/plugins/uptime/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts index 3cd0ffb1a2942..c8de623cb0a13 100644 --- a/x-pack/legacy/plugins/uptime/index.ts +++ b/x-pack/legacy/plugins/uptime/index.ts @@ -29,7 +29,7 @@ export const uptime = (kibana: any) => }), main: 'plugins/uptime/app', order: 8900, - url: '/app/uptime/', + url: '/app/uptime#/', }, home: ['plugins/uptime/register_feature'], }, From df21ec3fcfcf11bec871b3f740d2830a74a2b9b5 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Fri, 6 Dec 2019 22:24:03 +0100 Subject: [PATCH 22/35] Deprecate recompose part 1 (#50806) --- .../link_to/redirect_to_node_logs.test.tsx | 60 +- .../public/components/and_or_badge/index.tsx | 3 +- .../arrows/__snapshots__/index.test.tsx.snap | 15 +- .../public/components/arrows/index.test.tsx | 6 +- .../siem/public/components/arrows/index.tsx | 3 +- .../certificate_fingerprint/index.tsx | 3 +- .../public/components/charts/barchart.tsx | 3 +- .../__snapshots__/utility_bar.test.tsx.snap | 54 +- .../utility_bar_action.test.tsx.snap | 12 +- .../utility_bar_group.test.tsx.snap | 12 +- .../utility_bar_section.test.tsx.snap | 16 +- .../utility_bar_text.test.tsx.snap | 8 +- .../utility_bar/utility_bar.test.tsx | 2 +- .../utility_bar/utility_bar.tsx | 3 +- .../utility_bar/utility_bar_action.test.tsx | 2 +- .../utility_bar/utility_bar_action.tsx | 2 + .../utility_bar/utility_bar_group.test.tsx | 2 +- .../utility_bar/utility_bar_group.tsx | 1 + .../utility_bar/utility_bar_section.test.tsx | 2 +- .../utility_bar/utility_bar_section.tsx | 1 + .../utility_bar/utility_bar_text.test.tsx | 2 +- .../utility_bar/utility_bar_text.tsx | 1 + .../public/components/direction/index.tsx | 3 +- .../drag_drop_context_wrapper.test.tsx.snap | 819 +++--- .../draggable_wrapper.test.tsx.snap | 457 +--- .../droppable_wrapper.test.tsx.snap | 431 +--- .../drag_drop_context_wrapper.test.tsx | 2 +- .../drag_drop_context_wrapper.tsx | 2 + .../drag_and_drop/draggable_wrapper.test.tsx | 2 +- .../drag_and_drop/draggable_wrapper.tsx | 2 + .../drag_and_drop/droppable_wrapper.test.tsx | 2 +- .../drag_and_drop/droppable_wrapper.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 47 +- .../draggables/field_badge/index.tsx | 4 +- .../components/draggables/index.test.tsx | 48 +- .../public/components/draggables/index.tsx | 7 +- .../siem/public/components/duration/index.tsx | 3 +- .../__snapshots__/embeddable.test.tsx.snap | 12 +- .../embeddable_header.test.tsx.snap | 23 +- .../embeddables/embeddable.test.tsx | 9 +- .../embeddables/embeddable_header.test.tsx | 6 +- .../point_tool_tip_content.test.tsx.snap | 26 +- .../point_tool_tip_content.test.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 22 +- .../public/components/empty_page/index.tsx | 3 +- .../__snapshots__/event_details.test.tsx.snap | 2190 ++++++++++++----- .../__snapshots__/json_view.test.tsx.snap | 192 +- .../event_details/event_details.test.tsx | 24 +- .../components/event_details/json_view.tsx | 3 +- .../components/external_link_icon/index.tsx | 3 +- .../field_renderers.test.tsx.snap | 414 +--- .../field_renderers/field_renderers.test.tsx | 26 +- .../field_renderers/field_renderers.tsx | 5 +- .../components/fields_browser/category.tsx | 3 +- .../fields_browser/category_title.tsx | 47 +- .../components/fields_browser/fields_pane.tsx | 3 +- .../components/fields_browser/header.tsx | 7 +- .../filters_global.test.tsx.snap | 14 +- .../filters_global/filters_global.test.tsx | 3 +- .../filters_global/filters_global.tsx | 3 +- .../flow_direction_select.test.tsx.snap | 29 +- .../flow_target_select.test.tsx.snap | 34 +- .../flow_controls/flow_direction_select.tsx | 3 +- .../flow_controls/flow_target_select.tsx | 3 +- .../flyout/__snapshots__/index.test.tsx.snap | 22 +- .../public/components/flyout/index.test.tsx | 2 +- .../siem/public/components/flyout/index.tsx | 2 + .../pane/__snapshots__/index.test.tsx.snap | 34 +- .../components/flyout/pane/index.test.tsx | 2 +- .../public/components/flyout/pane/index.tsx | 2 + .../components/formatted_bytes/index.test.tsx | 4 + .../__snapshots__/index.test.tsx.snap | 6 +- .../components/formatted_date/index.test.tsx | 3 +- .../components/formatted_date/index.tsx | 5 +- .../components/formatted_duration/index.tsx | 3 +- .../formatted_duration/tooltip/index.tsx | 5 +- .../public/components/formatted_ip/index.tsx | 7 +- .../__snapshots__/index.test.tsx.snap | 106 +- .../components/header_global/index.test.tsx | 7 +- .../__snapshots__/index.test.tsx.snap | 58 +- .../components/header_page/index.test.tsx | 20 +- .../__snapshots__/index.test.tsx.snap | 27 +- .../components/header_section/index.test.tsx | 6 +- .../public/components/help_menu/index.tsx | 5 +- .../siem/public/components/inspect/index.tsx | 3 +- .../ip/__snapshots__/index.test.tsx.snap | 4 +- .../siem/public/components/ip/index.tsx | 3 +- .../components/ja3_fingerprint/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 21 +- .../components/link_icon/index.test.tsx | 8 +- .../public/components/link_to/link_to.tsx | 3 +- .../siem/public/components/links/index.tsx | 40 +- .../loader/__snapshots__/index.test.tsx.snap | 33 +- .../siem/public/components/loader/index.tsx | 4 +- .../siem/public/components/loading/index.tsx | 3 +- .../localized_date_tooltip/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 46 +- .../siem/public/components/markdown/index.tsx | 87 +- .../entity_draggable.test.tsx.snap | 2 +- .../__snapshots__/anomaly_score.test.tsx.snap | 2 +- .../draggable_score.test.tsx.snap | 4 +- .../public/components/navigation/index.tsx | 2 + .../netflow/__snapshots__/index.test.tsx.snap | 452 ++-- .../components/netflow/fingerprints/index.tsx | 3 +- .../public/components/netflow/index.test.tsx | 5 +- .../siem/public/components/netflow/index.tsx | 3 +- .../duration_event_start_end.tsx | 3 +- .../netflow/netflow_columns/index.tsx | 3 +- .../netflow/netflow_columns/user_process.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 49 +- .../__snapshots__/new_note.test.tsx.snap | 55 +- .../components/notes/add_note/index.tsx | 5 +- .../components/notes/add_note/new_note.tsx | 3 +- .../siem/public/components/notes/columns.tsx | 3 +- .../siem/public/components/notes/helpers.tsx | 3 +- .../components/notes/note_card/index.tsx | 3 +- .../notes/note_card/note_card_body.tsx | 3 +- .../notes/note_card/note_card_header.tsx | 3 +- .../notes/note_card/note_created.tsx | 3 +- .../delete_timeline_modal.tsx | 3 +- .../components/open_timeline/index.test.tsx | 1 - .../note_previews/index.test.tsx | 16 +- .../open_timeline/note_previews/index.tsx | 6 +- .../note_previews/note_preview.tsx | 3 +- .../open_timeline/open_timeline.tsx | 3 +- .../open_timeline_modal_body.tsx | 3 +- .../open_timeline/search_row/index.tsx | 3 +- .../timelines_table/common_columns.test.tsx | 8 +- .../timelines_table/common_columns.tsx | 4 +- .../open_timeline/timelines_table/index.tsx | 3 +- .../open_timeline/title_row/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 58 +- .../index.test.tsx | 46 +- .../__snapshots__/index.test.tsx.snap | 6 +- .../histogram_signals/index.test.tsx | 2 +- .../hosts/authentications_table/index.tsx | 3 +- .../page/hosts/first_last_seen_host/index.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 342 ++- .../page/hosts/host_overview/index.test.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 314 ++- .../page/hosts/hosts_table/index.test.tsx | 2 +- .../page/hosts/hosts_table/index.tsx | 2 + .../__snapshots__/index.test.tsx.snap | 342 ++- .../uncommon_process_table/index.test.tsx | 2 +- .../hosts/uncommon_process_table/index.tsx | 5 +- .../flow_target_select_connected/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 266 +- .../page/network/ip_overview/index.test.tsx | 2 +- .../page/network/ip_overview/index.tsx | 3 +- .../is_ptr_included.test.tsx.snap | 6 +- .../network_dns_table/is_ptr_included.tsx | 3 +- .../page/overview/overview_host/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 251 +- .../overview/overview_host_stats/index.tsx | 5 +- .../page/overview/overview_network/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 155 +- .../overview/overview_network_stats/index.tsx | 5 +- .../siem/public/components/pin/index.tsx | 23 +- .../port/__snapshots__/index.test.tsx.snap | 18 +- .../public/components/port/index.test.tsx | 4 +- .../siem/public/components/port/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 32 +- .../components/progress_inline/index.test.tsx | 9 +- .../__snapshots__/index.test.tsx.snap | 21 +- .../components/resize_handle/index.test.tsx | 16 +- .../__snapshots__/index.test.tsx.snap | 17 +- .../components/skeleton_row/index.test.tsx | 6 +- .../source_destination/geo_fields.tsx | 5 +- .../components/source_destination/index.tsx | 3 +- .../source_destination/ip_with_port.tsx | 49 +- .../components/source_destination/network.tsx | 3 +- .../source_destination_arrows.tsx | 7 +- .../source_destination_with_arrows.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 306 +-- .../__snapshots__/index.test.tsx.snap | 12 +- .../public/components/subtitle/index.test.tsx | 6 +- .../__snapshots__/helpers.test.tsx.snap | 52 +- .../public/components/tables/helpers.test.tsx | 6 +- .../timeline/auto_save_warning/index.tsx | 3 +- .../body/column_headers/actions/index.tsx | 5 +- .../column_headers/events_select/helpers.tsx | 3 +- .../column_headers/events_select/index.tsx | 3 +- .../filter/__snapshots__/index.test.tsx.snap | 12 +- .../body/column_headers/filter/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 76 +- .../header_tooltip_content/index.tsx | 3 +- .../column_headers/range_picker/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 9 +- .../body/column_headers/text_filter/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 350 ++- .../body/data_driven_columns/index.test.tsx | 20 +- .../empty_column_renderer.test.tsx.snap | 2 +- .../formatted_field.test.tsx.snap | 5 +- .../get_column_renderer.test.tsx.snap | 2 +- .../host_working_dir.test.tsx.snap | 37 +- .../plain_column_renderer.test.tsx.snap | 2 +- .../process_draggable.test.tsx.snap | 25 +- .../user_host_working_dir.test.tsx.snap | 52 +- .../generic_details.test.tsx.snap | 218 +- .../generic_file_details.test.tsx.snap | 30 +- .../primary_secondary_user_info.test.tsx.snap | 3 +- .../renderers/auditd/generic_details.test.tsx | 18 +- .../body/renderers/auditd/generic_details.tsx | 5 +- .../auditd/generic_file_details.test.tsx | 38 +- .../renderers/auditd/generic_file_details.tsx | 5 +- .../auditd/primary_secondary_user_info.tsx | 5 +- .../auditd/session_user_host_working_dir.tsx | 3 +- .../body/renderers/formatted_field.tsx | 3 +- .../body/renderers/host_working_dir.tsx | 9 +- .../timeline/body/renderers/netflow.tsx | 8 +- .../body/renderers/process_draggable.test.tsx | 44 +- .../body/renderers/process_draggable.tsx | 5 +- .../suricata_details.test.tsx.snap | 567 +---- .../suricata_signature.test.tsx.snap | 45 +- .../suricata/suricata_details.test.tsx | 14 +- .../renderers/suricata/suricata_details.tsx | 4 +- .../body/renderers/suricata/suricata_refs.tsx | 3 +- .../suricata/suricata_signature.test.tsx | 4 +- .../renderers/suricata/suricata_signature.tsx | 7 +- .../__snapshots__/auth_ssh.test.tsx.snap | 34 +- .../generic_details.test.tsx.snap | 188 +- .../generic_file_details.test.tsx.snap | 23 +- .../__snapshots__/package.test.tsx.snap | 44 +- .../body/renderers/system/auth_ssh.test.tsx | 124 +- .../body/renderers/system/auth_ssh.tsx | 3 +- .../body/renderers/system/generic_details.tsx | 5 +- .../system/generic_file_details.test.tsx | 16 +- .../renderers/system/generic_file_details.tsx | 5 +- .../body/renderers/system/package.test.tsx | 36 +- .../body/renderers/system/package.tsx | 3 +- .../renderers/user_host_working_dir.test.tsx | 40 +- .../body/renderers/user_host_working_dir.tsx | 3 +- .../__snapshots__/zeek_details.test.tsx.snap | 2 +- .../zeek_signature.test.tsx.snap | 180 +- .../body/renderers/zeek/zeek_details.test.tsx | 24 +- .../body/renderers/zeek/zeek_details.tsx | 24 +- .../renderers/zeek/zeek_signature.test.tsx | 21 +- .../body/renderers/zeek/zeek_signature.tsx | 67 +- .../sort_indicator.test.tsx.snap | 5 +- .../timeline/body/sort/sort_indicator.tsx | 3 +- .../data_providers.test.tsx.snap | 157 +- .../__snapshots__/empty.test.tsx.snap | 39 +- .../__snapshots__/provider.test.tsx.snap | 27 +- .../__snapshots__/providers.test.tsx.snap | 654 +++-- .../timeline/data_providers/empty.test.tsx | 6 +- .../timeline/data_providers/empty.tsx | 3 +- .../timeline/data_providers/index.tsx | 3 +- .../timeline/data_providers/provider.tsx | 3 +- .../data_providers/provider_badge.tsx | 3 +- .../provider_item_and_drag_drop.tsx | 3 +- .../data_providers/providers.test.tsx | 24 +- .../timeline/data_providers/providers.tsx | 3 +- .../components/timeline/footer/index.tsx | 36 +- .../timeline/footer/last_updated.tsx | 3 +- .../timeline/properties/helpers.tsx | 178 +- .../search_or_filter/search_or_filter.tsx | 3 +- .../components/with_hover_actions/index.tsx | 13 +- .../__snapshots__/index.test.tsx.snap | 60 +- .../components/wrapper_page/index.test.tsx | 8 +- .../public/components/wrapper_page/index.tsx | 4 +- .../public/containers/ip_overview/index.tsx | 3 +- .../containers/kpi_host_details/index.tsx | 3 +- .../public/containers/kpi_hosts/index.tsx | 5 +- .../public/containers/kpi_network/index.tsx | 5 +- .../overview/overview_host/index.tsx | 5 +- .../overview/overview_network/index.tsx | 63 +- .../lib/clipboard/with_copy_to_clipboard.tsx | 3 +- .../siem/public/mock/test_providers.tsx | 5 +- .../legacy/plugins/siem/public/pages/404.tsx | 4 +- .../plugins/siem/public/pages/home/index.tsx | 10 +- .../public/pages/hosts/hosts_empty_page.tsx | 3 +- .../pages/network/network_empty_page.tsx | 3 +- .../siem/public/pages/overview/summary.tsx | 3 +- x-pack/legacy/plugins/siem/public/routes.tsx | 4 +- 274 files changed, 6526 insertions(+), 6174 deletions(-) diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx index 1916e84ef21d3..7a63406bb419a 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx @@ -34,11 +34,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct container filter', () => { @@ -47,11 +47,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct pod filter', () => { @@ -60,11 +60,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct position', () => { @@ -75,11 +75,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct user-defined filter', () => { @@ -92,11 +92,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct custom source id', () => { @@ -107,11 +107,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx index 3548fb7c0e671..be449e3d422d9 100644 --- a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx @@ -6,7 +6,6 @@ import { EuiBadge } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import * as i18n from './translations'; @@ -39,7 +38,7 @@ export type AndOr = 'and' | 'or'; /** Displays AND / OR in a round badge */ // Ref: https://github.com/elastic/eui/issues/1655 -export const AndOrBadge = pure<{ type: AndOr }>(({ type }) => { +export const AndOrBadge = React.memo<{ type: AndOr }>(({ type }) => { return ( {type === 'and' ? i18n.AND : i18n.OR} diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap index 408bcac756f47..7702695520790 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap @@ -1,9 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`arrows ArrowBody renders correctly against snapshot 1`] = ` - - + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx index 8be0e7c267ec0..10d3c899562e8 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { mount } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -15,12 +15,12 @@ import { ArrowBody, ArrowHead } from '.'; describe('arrows', () => { describe('ArrowBody', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow( + const wrapper = mount( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('ArrowBody'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx b/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx index 6d5b464e0e886..dfc7645c564d2 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx @@ -6,7 +6,6 @@ import { EuiIcon } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; /** Renders the body (non-pointy part) of an arrow */ @@ -21,7 +20,7 @@ ArrowBody.displayName = 'ArrowBody'; export type ArrowDirection = 'arrowLeft' | 'arrowRight'; /** Renders the head of an arrow */ -export const ArrowHead = pure<{ +export const ArrowHead = React.memo<{ direction: ArrowDirection; }>(({ direction }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx index 37ec256ccd8c0..f8db7d754aab1 100644 --- a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx @@ -6,7 +6,6 @@ import { EuiText } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DraggableBadge } from '../draggables'; @@ -36,7 +35,7 @@ FingerprintLabel.displayName = 'FingerprintLabel'; * 'tls.client_certificate.fingerprint.sha1' * 'tls.server_certificate.fingerprint.sha1' */ -export const CertificateFingerprint = pure<{ +export const CertificateFingerprint = React.memo<{ eventId: string; certificateType: CertificateType; contextId: string; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx index 7218d7a497f19..99ad995e48852 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; - import { Chart, BarSeries, @@ -63,6 +62,7 @@ export const BarChartBaseComponent = ({ ...chartDefaultSettings, ...get('configs.settings', chartConfigs), }; + return chartConfigs.width && chartConfigs.height ? ( @@ -116,6 +116,7 @@ export const BarChartComponent = ({ }) => { const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); + return checkIfAnyValidSeriesExist(barChart) ? ( {({ measureRef, content: { height, width } }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap index 03a04983f9f86..f082dc4023e7a 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap @@ -1,32 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBar it renders 1`] = ` - - - - - - Test text - - - - - Test action - - - - - - - Test action - - - - - + + + + + Test text + + + + + Test action + + + + + + + Test action + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap index 470b40cd1d960..eb20ac217b300 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap @@ -1,11 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarAction it renders 1`] = ` - - - Test action - - + + Test action + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap index 62ff1b17dd55f..8ef7ee1cfe842 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap @@ -1,11 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarGroup it renders 1`] = ` - - - - Test text - - - + + + Test text + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap index f81717c892755..2fe3b8ac5c7aa 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap @@ -1,13 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarSection it renders 1`] = ` - - - - - Test text - - - - + + + + Test text + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap index 446b5556945d8..cf635ffa49c4c 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarText it renders 1`] = ` - - - Test text - - + + Test text + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx index 27688ec24530e..68522377bd847 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx @@ -47,7 +47,7 @@ describe('UtilityBar', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBar'))).toMatchSnapshot(); }); test('it applies border styles when border is true', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx index f226e0e055391..524769361ea9d 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx @@ -8,11 +8,12 @@ import React from 'react'; import { Bar, BarProps } from './styles'; -export interface UtilityBarProps extends BarProps { +interface UtilityBarProps extends BarProps { children: React.ReactNode; } export const UtilityBar = React.memo(({ border, children }) => ( {children} )); + UtilityBar.displayName = 'UtilityBar'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx index f71bdfda705d0..7921c1ef42200 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx @@ -22,7 +22,7 @@ describe('UtilityBarAction', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarAction'))).toMatchSnapshot(); }); test('it renders a popover', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx index 2ad48bc9b9c92..f695c33a37447 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx @@ -37,6 +37,7 @@ const Popover = React.memo( ); } ); + Popover.displayName = 'Popover'; export interface UtilityBarActionProps extends LinkIconProps { @@ -71,4 +72,5 @@ export const UtilityBarAction = React.memo( ) ); + UtilityBarAction.displayName = 'UtilityBarAction'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx index 84ad96c5a1e5e..294d27fa95b3d 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx @@ -24,6 +24,6 @@ describe('UtilityBarGroup', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarGroup'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx index 1e23fd3498199..723035df672a9 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx @@ -15,4 +15,5 @@ export interface UtilityBarGroupProps { export const UtilityBarGroup = React.memo(({ children }) => ( {children} )); + UtilityBarGroup.displayName = 'UtilityBarGroup'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx index 2dfc1d3b8d193..e0e0acc3a71c9 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx @@ -26,6 +26,6 @@ describe('UtilityBarSection', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarSection'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx index c457e6bc3dee0..42532c0355607 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx @@ -15,4 +15,5 @@ export interface UtilityBarSectionProps { export const UtilityBarSection = React.memo(({ children }) => ( {children} )); + UtilityBarSection.displayName = 'UtilityBarSection'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx index 0743e5cab02b4..29e1844bb2d4f 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx @@ -22,6 +22,6 @@ describe('UtilityBarText', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarText'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx index f8eb25f03d4ad..6195e008dbe27 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx @@ -15,4 +15,5 @@ export interface UtilityBarTextProps { export const UtilityBarText = React.memo(({ children }) => ( {children} )); + UtilityBarText.displayName = 'UtilityBarText'; diff --git a/x-pack/legacy/plugins/siem/public/components/direction/index.tsx b/x-pack/legacy/plugins/siem/public/components/direction/index.tsx index b5d6fcfc6cef7..9295e055f918d 100644 --- a/x-pack/legacy/plugins/siem/public/components/direction/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/direction/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { NetworkDirectionEcs } from '../../graphql/types'; import { DraggableBadge } from '../draggables'; @@ -56,7 +55,7 @@ export const getDirectionIcon = ( /** * Renders a badge containing the value of `network.direction` */ -export const DirectionBadge = pure<{ +export const DirectionBadge = React.memo<{ contextId: string; direction?: string | null; eventId: string; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap index 22c7b62711795..666a8249c27d8 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap @@ -1,426 +1,419 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DragDropContextWrapper rendering it renders against the snapshot 1`] = ` - - - - Drag drop context wrapper children - - - + "client.bytes": Object { + "aggregatable": true, + "category": "client", + "description": "Bytes sent from the client to the server.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.bytes", + "searchable": true, + "type": "number", + }, + "client.domain": Object { + "aggregatable": true, + "category": "client", + "description": "Client domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.domain", + "searchable": true, + "type": "string", + }, + "client.geo.country_iso_code": Object { + "aggregatable": true, + "category": "client", + "description": "Country ISO code.", + "example": "CA", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.geo.country_iso_code", + "searchable": true, + "type": "string", + }, + }, + }, + "cloud": Object { + "fields": Object { + "cloud.account.id": Object { + "aggregatable": true, + "category": "cloud", + "description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.", + "example": "666777888999", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.account.id", + "searchable": true, + "type": "string", + }, + "cloud.availability_zone": Object { + "aggregatable": true, + "category": "cloud", + "description": "Availability zone in which this host is running.", + "example": "us-east-1c", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.availability_zone", + "searchable": true, + "type": "string", + }, + }, + }, + "container": Object { + "fields": Object { + "container.id": Object { + "aggregatable": true, + "category": "container", + "description": "Unique container id.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.id", + "searchable": true, + "type": "string", + }, + "container.image.name": Object { + "aggregatable": true, + "category": "container", + "description": "Name of the image the container was built on.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.name", + "searchable": true, + "type": "string", + }, + "container.image.tag": Object { + "aggregatable": true, + "category": "container", + "description": "Container image tag.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.tag", + "searchable": true, + "type": "string", + }, + }, + }, + "destination": Object { + "fields": Object { + "destination.address": Object { + "aggregatable": true, + "category": "destination", + "description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.address", + "searchable": true, + "type": "string", + }, + "destination.bytes": Object { + "aggregatable": true, + "category": "destination", + "description": "Bytes sent from the destination to the source.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.bytes", + "searchable": true, + "type": "number", + }, + "destination.domain": Object { + "aggregatable": true, + "category": "destination", + "description": "Destination domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.domain", + "searchable": true, + "type": "string", + }, + "destination.ip": Object { + "aggregatable": true, + "category": "destination", + "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.ip", + "searchable": true, + "type": "ip", + }, + "destination.port": Object { + "aggregatable": true, + "category": "destination", + "description": "Port of the destination.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.port", + "searchable": true, + "type": "long", + }, + }, + }, + "event": Object { + "fields": Object { + "event.end": Object { + "aggregatable": true, + "category": "event", + "description": "event.end contains the date when the event ended or when the activity was last observed.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat-*", + "endgame-*", + "filebeat-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.end", + "searchable": true, + "type": "date", + }, + }, + }, + "source": Object { + "fields": Object { + "source.ip": Object { + "aggregatable": true, + "category": "source", + "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.ip", + "searchable": true, + "type": "ip", + }, + "source.port": Object { + "aggregatable": true, + "category": "source", + "description": "Port of the source.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.port", + "searchable": true, + "type": "long", + }, + }, + }, + } + } +> + Drag drop context wrapper children + `; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap index a240d5122ac9c..aa8214938c2b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap @@ -1,443 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DraggableWrapper rendering it renders against the snapshot 1`] = ` - - - - - - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap index 23a540f0ce3b3..7c6e321395fa5 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap @@ -1,430 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DroppableWrapper rendering it renders against the snapshot 1`] = ` - - - - - draggable wrapper content - - - - + + draggable wrapper content + `; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx index b8fba6fe2f6d8..1a8af9d99193a 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx @@ -28,7 +28,7 @@ describe('DragDropContextWrapper', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DragDropContextWrapper'))).toMatchSnapshot(); }); test('it renders the children', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index a3528158a0317..f9e6bfcf7c236 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -114,6 +114,8 @@ const mapStateToProps = (state: State) => { export const DragDropContextWrapper = connect(mapStateToProps)(DragDropContextWrapperComponent); +DragDropContextWrapper.displayName = 'DragDropContextWrapper'; + const onBeforeCapture = (before: BeforeCapture) => { const x = window.pageXOffset !== undefined diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index d9b78836b450e..008ece5c7e69c 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -30,7 +30,7 @@ describe('DraggableWrapper', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DraggableWrapper'))).toMatchSnapshot(); }); test('it renders the children passed to the render prop', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index c314785511201..809c46f7b53bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -275,6 +275,8 @@ export const DraggableWrapper = connect(null, { unRegisterProvider: dragAndDropActions.unRegisterProvider, })(DraggableWrapperComponent); +DraggableWrapper.displayName = 'DraggableWrapper'; + /** * Conditionally wraps children in an EuiPortal to ensure drag offsets are correct when dragging * from containers that have css transforms diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx index 859b30d2164dd..39abbdd4d4e38 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx @@ -30,7 +30,7 @@ describe('DroppableWrapper', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DroppableWrapper'))).toMatchSnapshot(); }); test('it renders the children when a render prop is not provided', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx index 3f789a39832f1..2b013a665af16 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx @@ -7,7 +7,6 @@ import { rgba } from 'polished'; import * as React from 'react'; import { Droppable } from 'react-beautiful-dnd'; -import { pure } from 'recompose'; import styled from 'styled-components'; interface Props { @@ -87,7 +86,7 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string `; ReactDndDropTarget.displayName = 'ReactDndDropTarget'; -export const DroppableWrapper = pure( +export const DroppableWrapper = React.memo( ({ children = null, droppableId, diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap index 1e9e89ad66641..63ba13306ecd8 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap @@ -1,29 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`draggables rendering it renders the default Badge 1`] = ` - - - A child of this - - + + + A child of this + + + `; exports[`draggables rendering it renders the default DefaultDraggable 1`] = ` - - - A child of this - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx index 5bff59494b9ad..90d8ad463b476 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx @@ -6,7 +6,6 @@ import { rgba } from 'polished'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; const Field = styled.div` @@ -28,11 +27,12 @@ Field.displayName = 'Field'; // Passing the styles directly to the component because the width is // being calculated and is recommended by Styled Components for performance // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 -export const DraggableFieldBadge = pure<{ fieldId: string; fieldWidth?: string }>( +export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: string }>( ({ fieldId, fieldWidth }) => ( {fieldId} ) ); + DraggableFieldBadge.displayName = 'DraggableFieldBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx index fb49329ba1501..d3dcba9526bdd 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx @@ -108,19 +108,15 @@ describe('draggables', () => { }); test('it returns null if value is undefined', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if value is null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); @@ -218,31 +214,27 @@ describe('draggables', () => { }); test('it returns null if value is undefined', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if value is null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx index 2f91cdc43b797..5b219dad9c841 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx @@ -6,7 +6,6 @@ import { EuiBadge, EuiBadgeProps, EuiToolTip, IconType } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import { Omit } from '../../../common/utility_types'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; @@ -50,7 +49,7 @@ export const getDefaultWhenTooltipIsUnspecified = ({ /** * Renders the content of the draggable, wrapped in a tooltip */ -const Content = pure<{ +const Content = React.memo<{ children?: React.ReactNode; field: string; tooltipContent?: React.ReactNode; @@ -83,7 +82,7 @@ Content.displayName = 'Content'; * prevent a tooltip from being displayed, or pass arbitrary content * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ -export const DefaultDraggable = pure( +export const DefaultDraggable = React.memo( ({ id, field, value, name, children, tooltipContent, queryValue }) => value != null ? ( & { * prevent a tooltip from being displayed, or pass arbitrary content * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ -export const DraggableBadge = pure( +export const DraggableBadge = React.memo( ({ contextId, eventId, diff --git a/x-pack/legacy/plugins/siem/public/components/duration/index.tsx b/x-pack/legacy/plugins/siem/public/components/duration/index.tsx index 06446a152bea8..15e6246f1f1ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/duration/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/duration/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { DefaultDraggable } from '../draggables'; import { FormattedDuration } from '../formatted_duration'; @@ -16,7 +15,7 @@ export const EVENT_DURATION_FIELD_NAME = 'event.duration'; * Renders draggable text containing the value of a field representing a * duration of time, (e.g. `event.duration`) */ -export const Duration = pure<{ +export const Duration = React.memo<{ contextId: string; eventId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap index f343316d88c46..b03670b2b1cd4 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap @@ -1,11 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Embeddable it renders 1`] = ` - - +
+

Test content

- - +
+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap index e88693b292a5d..6d02ccb1c6eb9 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap @@ -1,9 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EmbeddableHeader it renders 1`] = ` - - - +
+ + + +
+ Test title +
+
+
+
+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx index 49f5306dc1b60..c0d70754e78bd 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx @@ -9,7 +9,6 @@ import toJson from 'enzyme-to-json'; import React from 'react'; import '../../mock/ui_settings'; -import { TestProviders } from '../../mock'; import { Embeddable } from './embeddable'; jest.mock('../../lib/settings/use_kibana_ui_setting'); @@ -17,11 +16,9 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('Embeddable', () => { test('it renders', () => { const wrapper = shallow( - - -

{'Test content'}

-
-
+ +

{'Test content'}

+
); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx index 4536da3ba7b97..6387de30aa265 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx @@ -16,11 +16,7 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('EmbeddableHeader', () => { test('it renders', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap index 2ef4d9df89a1b..9d39b6e59365f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap @@ -1,18 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PointToolTipContent renders correctly against snapshot 1`] = ` - - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index 1733fb3aa7480..5e1eae1649b41 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -46,7 +46,7 @@ describe('PointToolTipContent', () => { /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('PointToolTipContentComponent'))).toMatchSnapshot(); }); test('renders array filter correctly', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap index 7e1da6ae7ace3..9b6bfb1752a20 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap @@ -1,9 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly 1`] = ` - + + + Do Something + + + + } + title={ +

+ My Super Title +

+ } /> `; diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx index 9c3dd462de153..ef2b76c9aad1c 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx @@ -6,7 +6,6 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, IconType } from '@elastic/eui'; import React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; const EmptyPrompt = styled(EuiEmptyPrompt)` @@ -29,7 +28,7 @@ interface EmptyPageProps { title: string; } -export const EmptyPage = pure( +export const EmptyPage = React.memo( ({ actionPrimaryIcon, actionPrimaryLabel, diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap index bfb10fc385c08..4cf7cbb43cdc7 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -1,692 +1,1544 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EventDetails rendering should match snapshot 1`] = ` - - + , + "id": "table-view", + "name": "Table", } } - columnHeaders={ + tabs={ Array [ Object { - "aggregatable": true, - "category": "base", - "columnHeaderType": "not-filtered", - "description": "Date/time when the event originated. + "content": , + "id": "table-view", + "name": "Table", }, Object { - "field": "destination.port", - "originalValue": 902, - "values": Array [ - "902", - ], + "content": , + "id": "json-view", + "name": "JSON View", }, ] } - id="Y-6TfmcB0WOhS6qyMv3s" - onUpdateColumns={[MockFunction]} - onViewSelected={[MockFunction]} - timelineId="test" - toggleColumn={[MockFunction]} - view="table-view" /> - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap index a788b60afd6b3..caa7853fd9ec0 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap @@ -1,150 +1,52 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`JSON View rendering should match snapshot 1`] = ` - + +}" + width="100%" + /> + `; diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx index fb1f9f0cd4e64..d8c0e46d8480b 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx @@ -21,19 +21,17 @@ describe('EventDetails', () => { describe('rendering', () => { test('should match snapshot', () => { const wrapper = shallow( - - - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx index 05690a0d20d92..519f56adff2d2 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx @@ -7,7 +7,6 @@ import { EuiCodeEditor } from '@elastic/eui'; import { set } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DetailItem } from '../../graphql/types'; @@ -23,7 +22,7 @@ const JsonEditor = styled.div` JsonEditor.displayName = 'JsonEditor'; -export const JsonView = pure(({ data }) => ( +export const JsonView = React.memo(({ data }) => ( (({ leftMargin = true }) => leftMargin ? ( diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap index 6ae9268966480..2ff93b2ecada4 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap @@ -1,220 +1,126 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Field Renderers #autonomousSystemRenderer it renders correctly against snapshot 1`] = ` - - + + + + - - - - - / - - - - - - + / + + + + +
`; exports[`Field Renderers #dateRenderer it renders correctly against snapshot 1`] = ` - - + - + `; exports[`Field Renderers #hostIdRenderer it renders correctly against snapshot 1`] = ` - - - - raspberrypi - - - + `; exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot 1`] = ` - - - - raspberrypi - - - + `; exports[`Field Renderers #locationRenderer it renders correctly against snapshot 1`] = ` - - + + + + ,  + - - - - ,  - - - - - + + + `; exports[`Field Renderers #reputationRenderer it renders correctly against snapshot 1`] = ` - talosIntelligence.com - + `; exports[`Field Renderers #whoisRenderer it renders correctly against snapshot 1`] = ` - - - iana.org - - + iana.org + `; diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx index 0fd63bc3f2bf2..2d69db82405ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx @@ -32,9 +32,7 @@ describe('Field Renderers', () => { describe('#locationRenderer', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - - {locationRenderer(['source.geo.city_name', 'source.geo.region_name'], mockData.complete)} - + locationRenderer(['source.geo.city_name', 'source.geo.region_name'], mockData.complete) ); expect(toJson(wrapper)).toMatchSnapshot(); @@ -59,9 +57,7 @@ describe('Field Renderers', () => { describe('#dateRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallow( - {dateRenderer(mockData.complete.source!.firstSeen)} - ); + const wrapper = shallow(dateRenderer(mockData.complete.source!.firstSeen)); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -78,9 +74,7 @@ describe('Field Renderers', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - - {autonomousSystemRenderer(mockData.complete.source!.autonomousSystem!, FlowTarget.source)} - + autonomousSystemRenderer(mockData.complete.source!.autonomousSystem!, FlowTarget.source) ); expect(toJson(wrapper)).toMatchSnapshot(); @@ -113,9 +107,7 @@ describe('Field Renderers', () => { ip: null, }; test('it renders correctly against snapshot', () => { - const wrapper = shallow( - {hostNameRenderer(mockData.complete.host, '10.10.10.10')} - ); + const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -158,9 +150,7 @@ describe('Field Renderers', () => { ip: ['10.10.10.10'], }; test('it renders correctly against snapshot', () => { - const wrapper = shallow( - {hostNameRenderer(mockData.complete.host, '10.10.10.10')} - ); + const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -194,9 +184,7 @@ describe('Field Renderers', () => { describe('#whoisRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallowWithIntl( - {whoisRenderer('10.10.10.10')} - ); + const wrapper = shallowWithIntl(whoisRenderer('10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -208,7 +196,7 @@ describe('Field Renderers', () => { {reputationRenderer('10.10.10.10')} ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DragDropContext'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx index 5df961dfceeb5..80d68dfe1b731 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx @@ -8,9 +8,8 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiText } from ' import { FormattedMessage } from '@kbn/i18n/react'; import { getOr } from 'lodash/fp'; import React, { Fragment, useState } from 'react'; -import { pure } from 'recompose'; - import styled from 'styled-components'; + import { AutonomousSystem, FlowTarget, HostEcsFields, IpOverviewData } from '../../graphql/types'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { DefaultDraggable } from '../draggables'; @@ -151,7 +150,7 @@ interface DefaultFieldRendererProps { // TODO: This causes breaks between elements until the ticket below is fixed // https://github.com/elastic/ingest-dev/issues/474 -export const DefaultFieldRenderer = pure( +export const DefaultFieldRenderer = React.memo( ({ attrName, displayCount = 1, diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx index 8d4e3b3928492..7b8451db2212f 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx @@ -5,7 +5,6 @@ */ import { EuiInMemoryTable } from '@elastic/eui'; -import { pure } from 'recompose'; import * as React from 'react'; import styled from 'styled-components'; @@ -33,7 +32,7 @@ interface Props { width: number; } -export const Category = pure( +export const Category = React.memo( ({ categoryId, filteredBrowserFields, fieldItems, timelineId, width }) => ( <> (({ filteredBrowserFields, categoryId, timelineId }) => ( - - - -
{categoryId}
-
-
- - - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - -
-)); +export const CategoryTitle = React.memo( + ({ filteredBrowserFields, categoryId, timelineId }) => ( + + + +
{categoryId}
+
+
+ + + + + {getFieldCount(filteredBrowserFields[categoryId])} + + + +
+ ) +); CategoryTitle.displayName = 'CategoryTitle'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx index 4cc5537bec343..170cf324ca6d8 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx @@ -5,7 +5,6 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { pure } from 'recompose'; import * as React from 'react'; import styled from 'styled-components'; @@ -59,7 +58,7 @@ type Props = Pick void; }; -export const FieldsPane = pure( +export const FieldsPane = React.memo( ({ columnHeaders, filteredBrowserFields, diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx index ae9109bffe0db..8acb19970c268 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx @@ -13,7 +13,6 @@ import { EuiTitle, } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -65,7 +64,7 @@ interface Props { timelineId: string; } -const CountRow = pure>(({ filteredBrowserFields }) => ( +const CountRow = React.memo>(({ filteredBrowserFields }) => ( >(({ filteredBrowserFi CountRow.displayName = 'CountRow'; -const TitleRow = pure<{ +const TitleRow = React.memo<{ isEventViewer?: boolean; onOutsideClick: () => void; onUpdateColumns: OnUpdateColumns; @@ -121,7 +120,7 @@ const TitleRow = pure<{ TitleRow.displayName = 'TitleRow'; -export const Header = pure( +export const Header = React.memo( ({ isEventViewer, isSearching, diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap index 56432cb25c189..35fe74abff284 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap @@ -1,9 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`rendering renders correctly 1`] = ` - -

- Additional filters here. -

-
+ + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx index adbd904c5c325..7f377a57c3e9b 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx @@ -9,7 +9,7 @@ import toJson from 'enzyme-to-json'; import React from 'react'; import '../../mock/match_media'; -import { FiltersGlobal } from './index'; +import { FiltersGlobal } from './filters_global'; describe('rendering', () => { test('renders correctly', () => { @@ -18,6 +18,7 @@ describe('rendering', () => {

{'Additional filters here.'}

); + expect(toJson(wrapper)).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx index bdda8497a8bcb..edf6f7f01ab2e 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx @@ -7,7 +7,6 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; import { Sticky } from 'react-sticky'; -import { pure } from 'recompose'; import styled, { css } from 'styled-components'; import { gutterTimeline } from '../../lib/helpers'; @@ -42,7 +41,7 @@ export interface FiltersGlobalProps { children: React.ReactNode; } -export const FiltersGlobal = pure(({ children }) => ( +export const FiltersGlobal = React.memo(({ children }) => ( {({ style, isSticky }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap index 9553ec5b7654e..ee76657c8d27a 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap @@ -1,8 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Select Flow Direction rendering it renders the basic group button for uni-direction and bi-direction 1`] = ` - + + + Unidirectional + + + Bidirectional + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap index 46053008ea09c..a9b48c8ee16be 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap @@ -1,11 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FlowTargetSelect Component rendering it renders the FlowTargetSelect 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx b/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx index d5370c218a2de..2b826164063be 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx @@ -7,7 +7,6 @@ import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; import React from 'react'; -import { pure } from 'recompose'; import { FlowDirection } from '../../graphql/types'; import * as i18n from './translations'; @@ -17,7 +16,7 @@ interface Props { onChangeDirection: (value: FlowDirection) => void; } -export const FlowDirectionSelect = pure(({ onChangeDirection, selectedDirection }) => ( +export const FlowDirectionSelect = React.memo(({ onChangeDirection, selectedDirection }) => ( ( +export const FlowTargetSelect = React.memo( ({ id, isLoading = false, diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap index 3aa9fd1b962b5..abdc4f4681294 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap @@ -1,16 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Flyout rendering it renders correctly against snapshot 1`] = ` - - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index ddc3e4f15938a..86a8952a10efa 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -37,7 +37,7 @@ describe('Flyout', () => { /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Flyout'))).toMatchSnapshot(); }); test('it renders the default flyout state as a button', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index aae8f67997156..2d347830d5b1b 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -124,3 +124,5 @@ const mapStateToProps = (state: State, { timelineId }: OwnProps) => { export const Flyout = connect(mapStateToProps, { showTimeline: timelineActions.showTimeline, })(FlyoutComponent); + +Flyout.displayName = 'Flyout'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap index 31eaf4f56d7bc..efa682cd4d18e 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -1,22 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Pane renders correctly against snapshot 1`] = ` - - - - I am a child of flyout - - - + + + I am a child of flyout + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 65233e55901ff..acea2d1cce468 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -44,7 +44,7 @@ describe('Pane', () => { ); - expect(toJson(EmptyComponent)).toMatchSnapshot(); + expect(toJson(EmptyComponent.find('Pane'))).toMatchSnapshot(); }); test('it should NOT let the flyout expand to take up the full width of the element that contains it', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index 4b5ceb25befa4..f2f0cf4f980f3 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -182,3 +182,5 @@ FlyoutPaneComponent.displayName = 'FlyoutPaneComponent'; export const Pane = connect(null, { applyDeltaToWidth: timelineActions.applyDeltaToWidth, })(FlyoutPaneComponent); + +Pane.displayName = 'Pane'; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx index 71820c62dd528..a517820361f9f 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx @@ -21,6 +21,10 @@ jest.mock('../../lib/settings/use_kibana_ui_setting', () => ({ describe('formatted_bytes', () => { describe('PreferenceFormattedBytes', () => { describe('rendering', () => { + beforeEach(() => { + mockUseKibanaUiSetting.mockClear(); + }); + const bytes = '2806422'; test('renders correctly against snapshot', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap index 0f9cf1ba89f9c..d196a23bff5bf 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`formatted_date PreferenceFormattedDate rendering renders correctly against snapshot 1`] = ` - +> + 2019-02-25T22:27:05.000Z + `; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx index bb0b947f149f4..df361a06d3805 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx @@ -38,7 +38,8 @@ describe('formatted_date', () => { .format(config.dateFormat); test('renders correctly against snapshot', () => { - const wrapper = shallow(); + mockUseKibanaUiSetting.mockImplementation(() => [null]); + const wrapper = mount(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx index 32c064096fcf9..37bf3653f3b62 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx @@ -7,7 +7,6 @@ import moment from 'moment-timezone'; import * as React from 'react'; import { FormattedRelative } from '@kbn/i18n/react'; -import { pure } from 'recompose'; import { DEFAULT_DATE_FORMAT, @@ -19,7 +18,7 @@ import { getOrEmptyTagFromValue } from '../empty_value'; import { LocalizedDateTooltip } from '../localized_date_tooltip'; import { getMaybeDate } from './maybe_date'; -export const PreferenceFormattedDate = pure<{ value: Date }>(({ value }) => { +export const PreferenceFormattedDate = React.memo<{ value: Date }>(({ value }) => { const [dateFormat] = useKibanaUiSetting(DEFAULT_DATE_FORMAT); const [dateFormatTz] = useKibanaUiSetting(DEFAULT_DATE_FORMAT_TZ); const [timezone] = useKibanaUiSetting(DEFAULT_TIMEZONE_BROWSER); @@ -43,7 +42,7 @@ PreferenceFormattedDate.displayName = 'PreferenceFormattedDate'; * - a long representation of the date that includes the day of the week (e.g. Thursday, March 21, 2019 6:47pm) * - the raw date value (e.g. 2019-03-22T00:47:46Z) */ -export const FormattedDate = pure<{ +export const FormattedDate = React.memo<{ fieldName: string; value?: string | number | null; }>( diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx index c97fc7bdc2428..8afbafe57af4a 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx @@ -5,12 +5,11 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { getFormattedDurationString } from './helpers'; import { FormattedDurationTooltip } from './tooltip'; -export const FormattedDuration = pure<{ +export const FormattedDuration = React.memo<{ maybeDurationNanoseconds: string | number | object | undefined | null; tooltipTitle?: string; }>(({ maybeDurationNanoseconds, tooltipTitle }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx index 08f4a412caf51..1372b3ef10920 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx @@ -6,7 +6,6 @@ import { EuiToolTip } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; @@ -18,7 +17,7 @@ const P = styled.p` P.displayName = 'P'; -export const FormattedDurationTooltipContent = pure<{ +export const FormattedDurationTooltipContent = React.memo<{ maybeDurationNanoseconds: string | number | object | undefined | null; tooltipTitle?: string; }>(({ maybeDurationNanoseconds, tooltipTitle }) => ( @@ -35,7 +34,7 @@ export const FormattedDurationTooltipContent = pure<{ FormattedDurationTooltipContent.displayName = 'FormattedDurationTooltipContent'; -export const FormattedDurationTooltip = pure<{ +export const FormattedDurationTooltip = React.memo<{ children: JSX.Element; maybeDurationNanoseconds: string | number | object | undefined | null; tooltipTitle?: string; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx index 81f5cbfe2308b..8dcb558122d01 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx @@ -6,7 +6,6 @@ import { isArray, isEmpty, isString, uniq } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; @@ -60,7 +59,7 @@ const getDataProvider = ({ and: [], }); -const NonDecoratedIp = pure<{ +const NonDecoratedIp = React.memo<{ contextId: string; eventId: string; fieldName: string; @@ -92,7 +91,7 @@ const NonDecoratedIp = pure<{ NonDecoratedIp.displayName = 'NonDecoratedIp'; -const AddressLinks = pure<{ +const AddressLinks = React.memo<{ addresses: string[]; contextId: string; eventId: string; @@ -128,7 +127,7 @@ const AddressLinks = pure<{ AddressLinks.displayName = 'AddressLinks'; -export const FormattedIp = pure<{ +export const FormattedIp = React.memo<{ contextId: string; eventId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap index 665a5c75f3684..849f3616524cc 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap @@ -1,7 +1,107 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderGlobal it renders 1`] = ` - - - + + + + + + + + + + + + + + + + + + + + + + Add data + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx index ebd1da634ed1a..b3eb599af9407 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx @@ -8,7 +8,6 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import React from 'react'; -import { TestProviders } from '../../mock'; import '../../mock/match_media'; import '../../mock/ui_settings'; import { HeaderGlobal } from './index'; @@ -23,11 +22,7 @@ jest.mock('../search_bar', () => ({ describe('HeaderGlobal', () => { test('it renders', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap index 0fe2890dc9f24..a91d8fce87dac 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap @@ -1,23 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderPage it renders 1`] = ` - - + -

- Test supplement -

-
-
+ + +

+ Test title + + +

+
+ + +
+ +

+ Test supplement +

+
+ + `; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx index 9c50a915b7ba8..c20f3c7185e66 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx @@ -18,17 +18,15 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('HeaderPage', () => { test('it renders', () => { const wrapper = shallow( - - -

{'Test supplement'}

-
-
+ +

{'Test supplement'}

+
); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap index ecd2b15a841f6..d4c3763f51460 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap @@ -1,9 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderSection it renders 1`] = ` - - - +
+ + + + + +

+ Test title +

+
+
+
+
+
+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx index 4a6da9c80968f..8606758c68d2c 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx @@ -17,11 +17,7 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('HeaderSection', () => { test('it renders', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx b/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx index 43fd8e653f3d8..d42ee08e86407 100644 --- a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect } from 'react'; -import { pure } from 'recompose'; +import React, { useEffect } from 'react'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; -export const HelpMenu = pure<{}>(() => { +export const HelpMenu = React.memo(() => { useEffect(() => { chrome.helpExtension.set({ appName: i18n.translate('xpack.siem.chrome.help.appName', { diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx index 56bd86310acad..6908aba542e4c 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx @@ -11,7 +11,6 @@ import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import styled from 'styled-components'; -import { pure } from 'recompose'; import { inputsModel, inputsSelectors, State } from '../../store'; import { InputsModelId } from '../../store/inputs/constants'; import { inputsActions } from '../../store/inputs'; @@ -58,7 +57,7 @@ interface InspectButtonDispatch { type InspectButtonProps = OwnProps & InspectButtonReducer & InspectButtonDispatch; -const InspectButtonComponent = pure( +const InspectButtonComponent = React.memo( ({ compact = false, inputId = 'global', diff --git a/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap index d75a0f054775a..0199742242e59 100644 --- a/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap @@ -1,10 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Port renders correctly against snapshot 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/ip/index.tsx b/x-pack/legacy/plugins/siem/public/components/ip/index.tsx index ceec48951a198..8c327989963b4 100644 --- a/x-pack/legacy/plugins/siem/public/components/ip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ip/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; @@ -18,7 +17,7 @@ const IP_FIELD_TYPE = 'ip'; * Renders text containing a draggable IP address (e.g. `source.ip`, * `destination.ip`) that contains a hyperlink */ -export const Ip = pure<{ +export const Ip = React.memo<{ contextId: string; eventId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx index 3148efbb3050a..950ab252ad0bd 100644 --- a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DraggableBadge } from '../draggables'; @@ -27,7 +26,7 @@ Ja3FingerprintLabel.displayName = 'Ja3FingerprintLabel'; * using TLS traffic to be identified, which is possible because SSL * negotiations happen in the clear */ -export const Ja3Fingerprint = pure<{ +export const Ja3Fingerprint = React.memo<{ eventId: string; contextId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap index 5902768383cb0..c5086c8cde285 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap @@ -1,14 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`LinkIcon it renders 1`] = ` - - + + Test link - - + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx index 451db49028ee1..7f9133a0de7c0 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx @@ -17,11 +17,9 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('LinkIcon', () => { test('it renders', () => { const wrapper = shallow( - - - {'Test link'} - - + + {'Test link'} + ); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 0125b52e3ad33..5a7f6ef1274c9 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom'; -import { pure } from 'recompose'; import { SiemPageName } from '../../pages/home/types'; import { HostsTableType } from '../../store/hosts/model'; @@ -26,7 +25,7 @@ interface LinkToPageProps { match: RouteMatch<{}>; } -export const LinkToPage = pure(({ match }) => ( +export const LinkToPage = React.memo(({ match }) => ( ( +export const HostDetailsLink = React.memo<{ children?: React.ReactNode; hostName: string }>( ({ children, hostName }) => ( {children ? children : hostName} @@ -22,7 +21,7 @@ export const HostDetailsLink = pure<{ children?: React.ReactNode; hostName: stri HostDetailsLink.displayName = 'HostDetailsLink'; -export const IPDetailsLink = pure<{ children?: React.ReactNode; ip: string }>( +export const IPDetailsLink = React.memo<{ children?: React.ReactNode; ip: string }>( ({ children, ip }) => ( {children ? children : ip} @@ -33,7 +32,7 @@ export const IPDetailsLink = pure<{ children?: React.ReactNode; ip: string }>( IPDetailsLink.displayName = 'IPDetailsLink'; // External Links -export const GoogleLink = pure<{ children?: React.ReactNode; link: string }>( +export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( {children ? children : link} @@ -43,7 +42,7 @@ export const GoogleLink = pure<{ children?: React.ReactNode; link: string }>( GoogleLink.displayName = 'GoogleLink'; -export const PortOrServiceNameLink = pure<{ +export const PortOrServiceNameLink = React.memo<{ children?: React.ReactNode; portOrServiceName: number | string; }>(({ children, portOrServiceName }) => ( @@ -60,21 +59,22 @@ export const PortOrServiceNameLink = pure<{ PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; -export const Ja3FingerprintLink = pure<{ children?: React.ReactNode; ja3Fingerprint: string }>( - ({ children, ja3Fingerprint }) => ( - - {children ? children : ja3Fingerprint} - - ) -); +export const Ja3FingerprintLink = React.memo<{ + children?: React.ReactNode; + ja3Fingerprint: string; +}>(({ children, ja3Fingerprint }) => ( + + {children ? children : ja3Fingerprint} + +)); Ja3FingerprintLink.displayName = 'Ja3FingerprintLink'; -export const CertificateFingerprintLink = pure<{ +export const CertificateFingerprintLink = React.memo<{ children?: React.ReactNode; certificateFingerprint: string; }>(({ children, certificateFingerprint }) => ( @@ -91,7 +91,7 @@ export const CertificateFingerprintLink = pure<{ CertificateFingerprintLink.displayName = 'CertificateFingerprintLink'; -export const ReputationLink = pure<{ children?: React.ReactNode; domain: string }>( +export const ReputationLink = React.memo<{ children?: React.ReactNode; domain: string }>( ({ children, domain }) => ( ( +export const VirusTotalLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( VirusTotalLink.displayName = 'VirusTotalLink'; -export const WhoIsLink = pure<{ children?: React.ReactNode; domain: string }>( +export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>( ({ children, domain }) => ( {children ? children : domain} diff --git a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap index 440193c9e0dfd..0885f15b1efba 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap @@ -1,11 +1,36 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`rendering renders correctly 1`] = ` - - Loading - + + + + + + +

+ Loading +

+
+
+
+ `; diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx index 55628fe2e8d33..be2ce3dde951c 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx @@ -14,7 +14,6 @@ import { } from '@elastic/eui'; import { rgba } from 'polished'; import React from 'react'; -import { pure } from 'recompose'; import styled, { css } from 'styled-components'; const Aside = styled.aside<{ overlay?: boolean; overlayBackground?: string }>` @@ -56,9 +55,10 @@ export interface LoaderProps { overlay?: boolean; overlayBackground?: string; size?: EuiLoadingSpinnerSize; + children?: React.ReactChild; } -export const Loader = pure(({ children, overlay, overlayBackground, size }) => ( +export const Loader = React.memo(({ children, overlay, overlayBackground, size }) => (

WEl~pAO|}sd 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%0Du54txX_%FS1va8sHMvbj)8ogE}rrSp>*Zi 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 Syy76hA8B!Uu__V6!2bh!L@1yD literal 0 HcmV?d00001 diff --git a/docs/user/security/images/role-space-visualization.png b/docs/user/security/images/role-space-visualization.png new file mode 100644 index 0000000000000000000000000000000000000000..746af89c66e85d557632f3b0bee3f5a9ae3e88c5 GIT binary patch literal 98970 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 for +details on creating a space. +* **Data**: You can use <> or +live data. In the steps below, Filebeat and Metricbeat data are used. + +[float] +==== Steps + +With the requirements in mind, here are the steps that you will work +through in this tutorial: + +* Create a role named `mortgage-developer` +* Give the role permission to access the data in the relevant indices +* Give the role permission to create visualizations and dashboards +* Create the web developer's user account with the proper roles + +[float] +==== Create a role + +Go to **Management > Roles** +for an overview of your roles. This view provides actions +for you to create, edit, and delete roles. + +[role="screenshot"] +image::security/images/role-management.png["Role management"] + + +You can create as many roles as you like. Click *Create role* and +provide a name. Use `dev-mortgage` because this role is for a developer +working on the bank's mortgage application. + + +[float] +==== Give the role permission to access the data + +Access to data in indices is an index-level privilege, so in +*Index privileges*, add lines for the indices that contain the +data for this role. Two privileges are required: `read` and +`view_index_metadata`. All privileges are detailed in the +https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html[security privileges] documentation. + +In the screenshots, Filebeat and Metricbeat data is used, but you +should use the index patterns for your indices. + +[role="screenshot"] +image::security/images/role-index-privilege.png["Index privilege"] + +[float] +==== Give the role permission to create visualizations and dashboards + +By default, roles do not give Kibana privileges. Click **Add space +privilege** and associate this role with the `Dev Mortgage` space. + +To enable users with the `dev-mortgage` role to create visualizations +and dashboards, click *All* for *Visualize* and *Dashboard*. Also +assign *All* for *Discover* because it is common for developers +to create saved searches while designing visualizations. + +[role="screenshot"] +image::security/images/role-space-visualization.png["Associate space"] + +[float] +==== Create the developer's user account with the proper roles + +Go to **Management > Users** and click on **Create user** to create a +user. Give the user the `dev-mortgage` role +and the `monitoring-user` role, which is required for users of **Stack Monitoring**. + +[role="screenshot"] +image::security/images/role-new-user.png["Developer user"] + +Finally, have the developer log in and access the Dev Mortgage space +and create a new visualization. + +NOTE: If the user is assigned to only one space, they will automatically enter that space on login. + From c8c3e51e2bd238ea8e527285185c23b5b474c7d6 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 5 Dec 2019 22:10:16 -0700 Subject: [PATCH 03/35] skip flaky suite (#52246) --- .../legacy/plugins/graph/public/components/search_bar.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx index a0514576877d1..8d5e27f1e9aec 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx @@ -63,7 +63,8 @@ function wrapSearchBarInContext(testProps: OuterSearchBarProps) { ); } -describe('search_bar', () => { +// FLAKY: https://github.com/elastic/kibana/issues/52246 +describe.skip('search_bar', () => { const defaultProps = { isLoading: false, onQuerySubmit: jest.fn(), From 68cc4de804ba8ab020b887503eaaf918ac94414b Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 5 Dec 2019 23:36:10 -0700 Subject: [PATCH 04/35] [SIEM][Detection Engine] Adds signal data index per spaces through index naming conventions (#52237) ## Summary Changes the signals output index to be based on the user's space * Adds the ability to create a space based index through `POST /api/detection_engine/index` * Adds the existence API for the index through `HEAD /api/detection_engine/index` * Adds an index check during the creation of a rule, `POST api/detection_engine/rules` that will return a status of 400 with an error message if the index does not exist * Adds a new optional key in kibana.dev.yml of `xpack.siem.signalsIndex` for developers working together who need to segregate signals indexes. * Splits apart the ECS mappings and the signal mappings into separate files for easier maintenance. * Deprecates the defaultSignalsIndex (will remove it once the UI is updated) * Updates the README.md to remove the SIGNALS_INDEX environment variable * Updates the existing unit tests * Adds more unit tests unit tests For people writing the UI: --- How do I check for the existence of a signals index? See [scripts/signal_index_exists.sh](https://github.com/elastic/kibana/blob/28937ebe00bfc90129cf7e3ca1a04755c6029331/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh) ```sh HEAD /api/detection_engine/index ``` How do I create a new signals index if my user has correct privileges? See [scripts/post_signal_index.sh](https://github.com/elastic/kibana/blob/28937ebe00bfc90129cf7e3ca1a04755c6029331/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh) ```sh POST /api/detection_engine/index ``` How do I delete _everything_ of all signal indexes, policies, and templates for a particular space? See [scripts/delete_signal_index.sh](https://github.com/elastic/kibana/blob/28937ebe00bfc90129cf7e3ca1a04755c6029331/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh) ```sh DELETE /api/detection_engine/index ``` FAQ for people testing --- What is the name of the index, policy, etc... per space? If you're using the default space the index, policies, etc... will be: ```sh .siem-signals-default ``` If you're using a custom space such as `test-space` they will be: ```sh .siem-signals-test-space ``` If you set your `xpack.siem.signalsIndex` in your `kibana.dev.yml` to something such as: ```yml xpack.siem.signalsIndex: .siem-signals-frank-hassanabad ``` And use the default space it will be: ```sh .siem-signals-frank-hassanabad-default ``` And for a custom space such as `test-space` they will be: ```sh .siem-signals-frank-hassanabad-test-space ``` What is the policy that is being set? See: [signals_policy.json](https://github.com/elastic/kibana/blob/28937ebe00bfc90129cf7e3ca1a04755c6029331/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json) ```json { "policy": { "phases": { "hot": { "min_age": "0ms", "actions": { "rollover": { "max_size": "10gb", "max_age": "7d" } } } } } } ``` What is the boot strap index that is being set look like? See: [create_bootstrap_index.ts](https://github.com/elastic/kibana/blob/28937ebe00bfc90129cf7e3ca1a04755c6029331/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts) You should see this when running: ```sh ./get_signal_index.sh | less ``` ```json ".siem-signals-default-000001": { "aliases": { ".siem-signals-default": { "is_write_index": true } }, ``` What is the template that is being set look like? See: [get_signals_template.ts](https://github.com/elastic/kibana/blob/28937ebe00bfc90129cf7e3ca1a04755c6029331/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts) You should see this at the bottom when running: ```sh ./get_signal_index.sh ``` ```json "settings": { "index": { "lifecycle": { "name": ".siem-signals-default", "rollover_alias": ".siem-signals-default" }, "number_of_shards": "1", "provided_name": ".siem-signals-default-000001", "creation_date": "1575502837772", "number_of_replicas": "1", "uuid": "GB0h3AYRQD6AWl8OfNonJA", "version": { "created": "8000099" } } } ``` For more in-depth of testing of spaces using dev tools of Kibana --- Different testing scenarios involving having spaces set in the URL, vs not having spaces set. Also different testing scenarios involving having a developer based `xpack.siem.signalsIndex` being set vs not having one set and gettin the default of `.siem-signals` With a default space and kibana.dev.yml setting of: * xpack.siem.signalsIndex: .siem-signals-frank-hassanabad You can use dev tools to check the results after doing a `./post_signal_index.sh` ``` sh GET /_template/.siem-signals-frank-hassanabad-default GET /.siem-signals-frank-hassanabad-default-000001 GET /_ilm/policy/.siem-signals-frank-hassanabad-default GET /_alias/.siem-signals-frank-hassanabad-default ``` With a default space and no `kibana.dev.yml` setting, you can use dev tools to check the results after doing a `./post_signal_index.sh` ```sh GET /.siem-signals-default GET /_template/.siem-signals-default GET /.siem-signals-default-000001 GET /_ilm/policy/.siem-signals-default GET /_alias/.siem-signals-default ``` Setting a space through: ```sh export SPACE_URL=/s/test-space ``` With a default space and `kibana.dev.yml` setting using a user name such as mine: * xpack.siem.signalsIndex: .siem-signals-frank-hassanabad You can use dev tools to check the results after doing a `./post_signal_index.sh` ``` GET /.siem-signals-frank-hassanabad-test-space GET /_template/.siem-signals-frank-hassanabad-test-space GET /.siem-signals-frank-hassanabad-test-space-000001 GET /_ilm/policy/.siem-signals-frank-hassanabad-test-space GET /_alias/.siem-signals-frank-hassanabad-test-space ``` With a default space and no `kibana.dev.yml` setting, you can use dev tools to check the results after doing a `./post_signal_index.sh` ``` GET /.siem-signals-test-space GET /_template/.siem-signals-test-space GET /.siem-signals-default-test-space-000001 GET /_ilm/policy/.siem-signals-test-space GET /_alias/.siem-signals-test-space ``` ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../legacy/plugins/siem/common/constants.ts | 14 +- x-pack/legacy/plugins/siem/index.ts | 18 +- .../scripts/convert_saved_search_to_rules.js | 5 +- .../plugins/siem/server/kibana.index.ts | 14 +- .../server/lib/detection_engine/README.md | 31 ++- .../alerts/get_input_output_index.test.ts | 215 ++---------------- .../alerts/get_input_output_index.ts | 60 +---- .../alerts/rules_alert_type.ts | 13 +- .../index/create_bootstrap_index.ts | 32 +++ .../index/delete_all_index.ts | 18 ++ .../detection_engine/index/delete_policy.ts | 17 ++ .../detection_engine/index/delete_template.ts | 18 ++ .../index/get_index_exists.ts | 18 ++ .../index/get_policy_exists.ts | 29 +++ .../index/get_template_exists.ts | 18 ++ .../lib/detection_engine/index/read_index.ts | 18 ++ .../lib/detection_engine/index/set_policy.ts | 19 ++ .../detection_engine/index/set_template.ts | 20 ++ .../lib/detection_engine/index/types.ts | 7 + .../routes/__mocks__/_mock_server.ts | 9 +- .../routes/create_rules_route.test.ts | 8 +- .../routes/create_rules_route.ts | 155 +++++++------ .../routes/index/create_index_route.ts | 63 +++++ .../routes/index/delete_index_route.ts | 70 ++++++ .../index/ecs_mapping.json} | 180 --------------- .../routes/index/get_signals_template.test.ts | 31 +++ .../routes/index/get_signals_template.ts | 25 ++ .../routes/index/read_index_route.ts | 57 +++++ .../routes/index/signals_mapping.json | 186 +++++++++++++++ .../routes/index/signals_policy.json | 15 ++ .../lib/detection_engine/routes/utils.ts | 15 ++ .../scripts/check_env_variables.sh | 5 - .../scripts/delete_signal_index.sh | 6 +- ...ut_signal_index.sh => get_signal_index.sh} | 9 +- .../detection_engine/scripts/hard_reset.sh | 2 +- ...signal_mapping.sh => post_signal_index.sh} | 10 +- .../rules/filter_with_empty_query.json | 1 - .../scripts/rules/filter_without_query.json | 1 - .../scripts/signal_index_exists.sh | 16 ++ x-pack/legacy/plugins/siem/server/types.ts | 1 + 40 files changed, 888 insertions(+), 561 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/index/types.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{signals_mapping.json => routes/index/ecs_mapping.json} (91%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{put_signal_index.sh => get_signal_index.sh} (56%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{get_signal_mapping.sh => post_signal_index.sh} (53%) create mode 100755 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index e5d1fc83dac26..11b97738fcf52 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -15,7 +15,11 @@ export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults'; export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults'; export const DEFAULT_SIEM_TIME_RANGE = 'siem:timeDefaults'; export const DEFAULT_SIEM_REFRESH_INTERVAL = 'siem:refreshIntervalDefaults'; + +// DEPRECATED: THIS WILL BE REMOVED VERY SOON AND IS NO LONGER USED ON THE BACKEND +// TODO: Remove this as soon as no code is left that is pulling data from it. export const DEFAULT_SIGNALS_INDEX_KEY = 'siem:defaultSignalsIndex'; + export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; export const DEFAULT_MAX_SIGNALS = 100; export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; @@ -32,12 +36,18 @@ export const DEFAULT_INTERVAL_VALUE = 300000; // ms export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; /** - * Id for the SIGNALS alerting type + * Id for the signals alerting type */ export const SIGNALS_ID = `${APP_ID}.signals`; /** - * Detection engine route + * Detection engine routes */ export const DETECTION_ENGINE_URL = '/api/detection_engine'; export const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules`; +export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; + +/** + * Default signals index key for kibana.dev.yml + */ +export const SIGNALS_INDEX_KEY = 'signalsIndex'; diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 72b4ec588a5a4..fca4a13db8cb5 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; import { Server } from 'hapi'; +import { Root } from 'joi'; import { PluginInitializerContext } from 'src/core/server'; import { plugin } from './server'; @@ -24,6 +25,7 @@ import { DEFAULT_FROM, DEFAULT_TO, DEFAULT_SIGNALS_INDEX, + SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX_KEY, } from './common/constants'; import { defaultIndexPattern } from './default_index_pattern'; @@ -103,6 +105,8 @@ export const siem = (kibana: any) => { category: ['siem'], requiresPageReload: true, }, + // DEPRECATED: This should be removed once the front end is no longer using any parts of it. + // TODO: Remove this as soon as no code is left that is pulling data from it. [DEFAULT_SIGNALS_INDEX_KEY]: { name: i18n.translate('xpack.siem.uiSettings.defaultSignalsIndexLabel', { defaultMessage: 'Elasticsearch signals index', @@ -155,7 +159,11 @@ export const siem = (kibana: any) => { getInjectedUiAppVars, indexPatternsServiceFactory, injectUiAppVars, - plugins: { alerting: plugins.alerting, xpack_main: plugins.xpack_main }, + plugins: { + alerting: plugins.alerting, + xpack_main: plugins.xpack_main, + spaces: plugins.spaces, + }, route: route.bind(server), savedObjects, }; @@ -166,5 +174,13 @@ export const siem = (kibana: any) => { serverFacade ); }, + config(Joi: Root) { + return Joi.object() + .keys({ + enabled: Joi.boolean().default(true), + [SIGNALS_INDEX_KEY]: Joi.string().default(DEFAULT_SIGNALS_INDEX), + }) + .default(); + }, }); }; diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js index 3e1c5f51ebb5c..b282a8bf1e861 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js @@ -42,9 +42,10 @@ const allRulesNdJson = 'all_rules.ndjson'; // For converting, if you want to use these instead of rely on the defaults then // comment these in and use them for the script. Otherwise this is commented out // so we can utilize the defaults of input and output which are based on saved objects -// of siem:defaultIndex and siem:defaultSignalsIndex +// of siem:defaultIndex and your kibana.dev.yml setting of xpack.siem.signalsIndex. If +// the setting of xpack.siem.signalsIndex is not set it defaults to .siem-signals // const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; -// const OUTPUT_INDEX = process.env.SIGNALS_INDEX || '.siem-signals'; +// const OUTPUT_INDEX = '.siem-signals-some-other-index'; const walk = dir => { const list = fs.readdirSync(dir); diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index 3d73b9f4d90b0..66ec436ea418f 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -18,11 +18,14 @@ import { import { rulesAlertType } from './lib/detection_engine/alerts/rules_alert_type'; import { isAlertExecutor } from './lib/detection_engine/alerts/types'; import { createRulesRoute } from './lib/detection_engine/routes/create_rules_route'; +import { createIndexRoute } from './lib/detection_engine/routes/index/create_index_route'; +import { readIndexRoute } from './lib/detection_engine/routes/index/read_index_route'; import { readRulesRoute } from './lib/detection_engine/routes/read_rules_route'; import { findRulesRoute } from './lib/detection_engine/routes/find_rules_route'; import { deleteRulesRoute } from './lib/detection_engine/routes/delete_rules_route'; import { updateRulesRoute } from './lib/detection_engine/routes/update_rules_route'; import { ServerFacade } from './types'; +import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_index_route'; const APP_ID = 'siem'; @@ -43,15 +46,20 @@ export const initServerWithKibana = ( const libs = compose(kbnServer, mode); initServer(libs); - // Signals/Alerting Rules routes for - // routes such as ${DETECTION_ENGINE_RULES_URL} - // that have the REST endpoints of /api/detection_engine/rules + // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules + // All REST rule creation, deletion, updating, etc... createRulesRoute(kbnServer); readRulesRoute(kbnServer); updateRulesRoute(kbnServer); deleteRulesRoute(kbnServer); findRulesRoute(kbnServer); + // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index + // All REST index creation, policy management for spaces + createIndexRoute(kbnServer); + readIndexRoute(kbnServer); + deleteIndexRoute(kbnServer); + const xpackMainPlugin = kbnServer.plugins.xpack_main; xpackMainPlugin.registerFeature({ id: APP_ID, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index 75757bbaa0c1f..755727f9870f6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -2,11 +2,12 @@ README.md for developers working on the backend detection engine on how to get s using the CURL scripts in the scripts folder. The scripts rely on CURL and jq: -* [CURL](https://curl.haxx.se) -* [jq](https://stedolan.github.io/jq/) +- [CURL](https://curl.haxx.se) +- [jq](https://stedolan.github.io/jq/) Install curl and jq + ```sh brew update brew install curl @@ -21,7 +22,6 @@ export ELASTICSEARCH_USERNAME=${user} export ELASTICSEARCH_PASSWORD=${password} export ELASTICSEARCH_URL=https://${ip}:9200 export KIBANA_URL=http://localhost:5601 -export SIGNALS_INDEX=.siem-signals-${your user id} export TASK_MANAGER_INDEX=.kibana-task-manager-${your user id} export KIBANA_INDEX=.kibana-${your user id} ``` @@ -32,6 +32,12 @@ source `$HOME/.zshrc` or `${HOME}.bashrc` to ensure variables are set: source ~/.zshrc ``` +Open your `kibana.dev.yml` file and add these lines: + +```sh +xpack.siem.signalsIndex: .siem-signals-${your user id} +``` + Restart Kibana and ensure that you are using `--no-base-path` as changing the base path is a feature but will get in the way of the CURL scripts written as is. You should see alerting and actions starting up like so afterwards @@ -40,18 +46,11 @@ server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status changed f server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready ``` -Go into your SIEM Advanced settings and underneath the setting of `siem:defaultSignalsIndex`, set that to the same -value as you did with the environment variable of `${SIGNALS_INDEX}`, which should be `.siem-signals-${your user id}` - -``` -.siem-signals-${your user id} -``` - Go to the scripts folder `cd kibana/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts` and run: ```sh ./hard_reset.sh -./post_signal.sh +./post_rule.sh ``` which will: @@ -59,9 +58,9 @@ which will: - Delete any existing actions you have - Delete any existing alerts you have - Delete any existing alert tasks you have -- Delete any existing signal mapping you might have had. -- Add the latest signal index and its mappings using your settings from `${SIGNALS_INDEX}` environment variable. -- Posts the sample rule from `rules/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable +- Delete any existing signal mapping, policies, and template, you might have previously had. +- Add the latest signal index and its mappings using your settings from `kibana.dev.yml` environment variable of `xpack.siem.signalsIndex`. +- Posts the sample rule from `rules/root_or_admin_1.json` - The sample rule checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit Now you can run @@ -128,9 +127,9 @@ post rules to `test-space` you set `SPACE_URL` to be: export SPACE_URL=/s/test-space ``` -The `${SPACE_URL}` is in front of all the APIs to correctly create, modify, delete, and update +The `${SPACE_URL}` is in front of all the APIs to correctly create, modify, delete, and update them from within the defined space. If this variable is not defined the default which is the url of an -empty string will be used. +empty string will be used. Add the `.siem-signals-${your user id}` to your advanced SIEM settings to see any signals created which should update once every 5 minutes at this point. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts index 07eb7c885b443..bd7ba915af9b0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts @@ -5,13 +5,9 @@ */ import { savedObjectsClientMock } from 'src/core/server/mocks'; -import { - DEFAULT_SIGNALS_INDEX_KEY, - DEFAULT_INDEX_KEY, - DEFAULT_SIGNALS_INDEX, -} from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { AlertServices } from '../../../../../alerting/server/types'; -import { getInputOutputIndex, getOutputIndex, getInputIndex } from './get_input_output_index'; +import { getInputIndex } from './get_input_output_index'; import { defaultIndexPattern } from '../../../../default_index_pattern'; describe('get_input_output_index', () => { @@ -46,240 +42,61 @@ describe('get_input_output_index', () => { }); describe('getInputOutputIndex', () => { - test('Returns inputIndex as is if inputIndex and outputIndex are both passed in', async () => { + test('Returns inputIndex if inputIndex is passed in', async () => { savedObjectsClient.get = jest.fn().mockImplementation(() => ({ attributes: {}, })); - const { inputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - ['test-input-index-1'], - 'test-output-index' - ); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', ['test-input-index-1']); expect(inputIndex).toEqual(['test-input-index-1']); }); - test('Returns outputIndex as is if inputIndex and outputIndex are both passed in', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const { outputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - ['test-input-index-1'], - 'test-output-index' - ); - expect(outputIndex).toEqual('test-output-index'); - }); - - test('Returns inputIndex as is if inputIndex is defined but outputIndex is null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const { inputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - ['test-input-index-1'], - null - ); - expect(inputIndex).toEqual(['test-input-index-1']); - }); - - test('Returns outputIndex as is if inputIndex is null but outputIndex is defined', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const { outputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - null, - 'test-output-index' - ); - expect(outputIndex).toEqual('test-output-index'); - }); - - test('Returns a saved object outputIndex if both passed in are undefined', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', - }, - })); - const { outputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - undefined, - undefined - ); - expect(outputIndex).toEqual('.signals-test-index'); - }); - - test('Returns a saved object outputIndex if passed in outputIndex is undefined', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', - }, - })); - const { outputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - ['test-input-index-1'], - undefined - ); - expect(outputIndex).toEqual('.signals-test-index'); - }); - - test('Returns a saved object outputIndex default from constants if both passed in input and configuration are null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_SIGNALS_INDEX_KEY]: null, - }, - })); - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const { outputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); - expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); - }); - - test('Returns a saved object outputIndex default from constants if both passed in input and configuration are missing', async () => { - const { outputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - undefined, - undefined - ); - expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); - }); - - test('Returns a saved object inputIndex if passed in inputIndex and outputIndex are undefined', async () => { + test('Returns a saved object inputIndex if passed in inputIndex is undefined', async () => { savedObjectsClient.get = jest.fn().mockImplementation(() => ({ attributes: { [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], }, })); - const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); }); - test('Returns a saved object inputIndex if passed in inputIndex is undefined', async () => { + test('Returns a saved object inputIndex if passed in inputIndex is null', async () => { savedObjectsClient.get = jest.fn().mockImplementation(() => ({ attributes: { [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], }, })); - const { inputIndex } = await getInputOutputIndex( - servicesMock, - '8.0.0', - undefined, - 'output-index-1' - ); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); }); - test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration is null', async () => { + test('Returns a saved object inputIndex default from constants if inputIndex passed in is null and the key is also null', async () => { savedObjectsClient.get = jest.fn().mockImplementation(() => ({ attributes: { [DEFAULT_INDEX_KEY]: null, }, })); - const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); - expect(inputIndex).toEqual(defaultIndexPattern); - }); - - test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes is missing', async () => { - const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); expect(inputIndex).toEqual(defaultIndexPattern); }); - }); - - describe('getOutputIndex', () => { - test('test output index is returned when passed in as is', async () => { - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const outputIndex = getOutputIndex('output-index-1', mockConfiguration); - expect(outputIndex).toEqual('output-index-1'); - }); - - test('configured output index is returned when output index is null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_SIGNALS_INDEX_KEY]: '.siem-test-signals', - }, - })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const outputIndex = getOutputIndex(null, mockConfiguration); - expect(outputIndex).toEqual('.siem-test-signals'); - }); - test('output index from constants is returned when output index is null and so is the configuration', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_SIGNALS_INDEX_KEY]: null, - }, - })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const outputIndex = getOutputIndex(null, mockConfiguration); - expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); - }); - - test('output index from constants is returned when output index is null and configuration is missing', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const outputIndex = getOutputIndex(null, mockConfiguration); - expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); - }); - - test('output index from constants is returned when output index is null and attributes is missing', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const outputIndex = getOutputIndex(null, mockConfiguration); - expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); - }); - }); - - describe('getInputIndex', () => { - test('test input index is returned when passed in as is', async () => { - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const inputIndex = getInputIndex(['input-index-1'], mockConfiguration); - expect(inputIndex).toEqual(['input-index-1']); - }); - - test('configured input index is returned when input index is null', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: { - [DEFAULT_INDEX_KEY]: ['input-index-1', 'input-index-2'], - }, - })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const inputIndex = getInputIndex(null, mockConfiguration); - expect(inputIndex).toEqual(['input-index-1', 'input-index-2']); - }); - - test('input index from constants is returned when input index is null and so is the configuration', async () => { + test('Returns a saved object inputIndex default from constants if inputIndex passed in is undefined and the key is also null', async () => { savedObjectsClient.get = jest.fn().mockImplementation(() => ({ attributes: { [DEFAULT_INDEX_KEY]: null, }, })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const inputIndex = getInputIndex(null, mockConfiguration); + const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); expect(inputIndex).toEqual(defaultIndexPattern); }); - test('input index from constants is returned when input index is null and configuration is missing', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({ - attributes: {}, - })); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const inputIndex = getInputIndex(null, mockConfiguration); + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is undefined', async () => { + const inputIndex = await getInputIndex(servicesMock, '8.0.0', undefined); expect(inputIndex).toEqual(defaultIndexPattern); }); - test('input index from constants is returned when input index is null and attributes is missing', async () => { - savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); - const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); - const inputIndex = getInputIndex(null, mockConfiguration); + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes are missing and the index is null', async () => { + const inputIndex = await getInputIndex(servicesMock, '8.0.0', null); expect(inputIndex).toEqual(defaultIndexPattern); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts index 567ab27976d8d..624e012717820 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts @@ -4,27 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectAttributes } from 'src/core/server'; import { defaultIndexPattern } from '../../../../default_index_pattern'; import { AlertServices } from '../../../../../alerting/server/types'; -import { - DEFAULT_INDEX_KEY, - DEFAULT_SIGNALS_INDEX_KEY, - DEFAULT_SIGNALS_INDEX, -} from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -interface IndexObjectAttributes extends SavedObjectAttributes { - [DEFAULT_INDEX_KEY]: string[]; - [DEFAULT_SIGNALS_INDEX_KEY]: string; -} - -export const getInputIndex = ( - inputIndex: string[] | undefined | null, - configuration: SavedObject -): string[] => { +export const getInputIndex = async ( + services: AlertServices, + version: string, + inputIndex: string[] | null | undefined +): Promise => { if (inputIndex != null) { return inputIndex; } else { + const configuration = await services.savedObjectsClient.get('config', version); if (configuration.attributes != null && configuration.attributes[DEFAULT_INDEX_KEY] != null) { return configuration.attributes[DEFAULT_INDEX_KEY]; } else { @@ -32,41 +24,3 @@ export const getInputIndex = ( } } }; - -export const getOutputIndex = ( - outputIndex: string | undefined | null, - configuration: SavedObject -): string => { - if (outputIndex != null) { - return outputIndex; - } else { - if ( - configuration.attributes != null && - configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY] != null - ) { - return configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY]; - } else { - return DEFAULT_SIGNALS_INDEX; - } - } -}; - -export const getInputOutputIndex = async ( - services: AlertServices, - version: string, - inputIndex: string[] | null | undefined, - outputIndex: string | null | undefined -): Promise<{ - inputIndex: string[]; - outputIndex: string; -}> => { - if (inputIndex != null && outputIndex != null) { - return { inputIndex, outputIndex }; - } else { - const configuration = await services.savedObjectsClient.get('config', version); - return { - inputIndex: getInputIndex(inputIndex, configuration), - outputIndex: getOutputIndex(outputIndex, configuration), - }; - } -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts index 61fe9c7c22639..577de2ce0d532 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts @@ -16,7 +16,7 @@ import { buildEventsSearchQuery } from './build_events_query'; import { searchAfterAndBulkCreate } from './utils'; import { RuleAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; -import { getInputOutputIndex } from './get_input_output_index'; +import { getInputIndex } from './get_input_output_index'; export const rulesAlertType = ({ logger, @@ -84,12 +84,7 @@ export const rulesAlertType = ({ ? DEFAULT_SEARCH_AFTER_PAGE_SIZE : params.maxSignals; - const { inputIndex, outputIndex: signalsIndex } = await getInputOutputIndex( - services, - version, - index, - outputIndex - ); + const inputIndex = await getInputIndex(services, version, index); const esFilter = await getFilter({ type, filter, @@ -122,7 +117,7 @@ export const rulesAlertType = ({ noReIndexResult.hits.total.value } signals from the indexes of "${inputIndex.join( ', ' - )}" using signal rule "id: ${alertId}", "ruleId: ${ruleId}", pushing signals to index ${signalsIndex}` + )}" using signal rule "id: ${alertId}", "ruleId: ${ruleId}", pushing signals to index ${outputIndex}` ); } @@ -132,7 +127,7 @@ export const rulesAlertType = ({ services, logger, id: alertId, - signalsIndex, + signalsIndex: outputIndex, name, createdBy, updatedBy, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts new file mode 100644 index 0000000000000..9c8dca0cb370f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts @@ -0,0 +1,32 @@ +/* + * 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 { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +// See the reference(s) below on explanations about why -000001 was chosen and +// why the is_write_index is true as well as the bootstrapping step which is needed. +// Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/applying-policy-to-template.html +export const createBootstrapIndex = async ( + callWithRequest: CallWithRequest< + { path: string; method: 'PUT'; body: unknown }, + CallClusterOptions, + boolean + >, + index: string +): Promise => { + return callWithRequest('transport.request', { + path: `${index}-000001`, + method: 'PUT', + body: { + aliases: { + [index]: { + is_write_index: true, + }, + }, + }, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts new file mode 100644 index 0000000000000..6f16eb8fbdeb1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts @@ -0,0 +1,18 @@ +/* + * 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 { IndicesDeleteParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const deleteAllIndex = async ( + callWithRequest: CallWithRequest, + index: string +): Promise => { + return callWithRequest('indices.delete', { + index, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts new file mode 100644 index 0000000000000..153b9ae4e4136 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts @@ -0,0 +1,17 @@ +/* + * 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 { CallWithRequest } from './types'; + +export const deletePolicy = async ( + callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, {}, unknown>, + policy: string +): Promise => { + return callWithRequest('transport.request', { + path: `_ilm/policy/${policy}`, + method: 'DELETE', + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts new file mode 100644 index 0000000000000..b048dd27efb83 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts @@ -0,0 +1,18 @@ +/* + * 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 { IndicesDeleteTemplateParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const deleteTemplate = async ( + callWithRequest: CallWithRequest, + name: string +): Promise => { + return callWithRequest('indices.deleteTemplate', { + name, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts new file mode 100644 index 0000000000000..24164e894788a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts @@ -0,0 +1,18 @@ +/* + * 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 { IndicesExistsParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const getIndexExists = async ( + callWithRequest: CallWithRequest, + index: string +): Promise => { + return callWithRequest('indices.exists', { + index, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts new file mode 100644 index 0000000000000..847c32d9d61fb --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts @@ -0,0 +1,29 @@ +/* + * 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 { CallWithRequest } from './types'; + +export const getPolicyExists = async ( + callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, {}, unknown>, + policy: string +): Promise => { + try { + await callWithRequest('transport.request', { + path: `_ilm/policy/${policy}`, + method: 'GET', + }); + // Return true that there exists a policy which is not 404 or some error + // Since there is not a policy exists API, this is how we create one by calling + // into the API to get it if it exists or rely on it to throw a 404 + return true; + } catch (err) { + if (err.statusCode === 404) { + return false; + } else { + throw err; + } + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts new file mode 100644 index 0000000000000..482fc8d855828 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts @@ -0,0 +1,18 @@ +/* + * 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 { IndicesExistsTemplateParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const getTemplateExists = async ( + callWithRequest: CallWithRequest, + template: string +): Promise => { + return callWithRequest('indices.existsTemplate', { + name: template, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts new file mode 100644 index 0000000000000..6c9d529078a77 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts @@ -0,0 +1,18 @@ +/* + * 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 { IndicesGetSettingsParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const readIndex = async ( + callWithRequest: CallWithRequest, + index: string +): Promise => { + return callWithRequest('indices.get', { + index, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts new file mode 100644 index 0000000000000..2511984b412f3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts @@ -0,0 +1,19 @@ +/* + * 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 { CallWithRequest } from './types'; + +export const setPolicy = async ( + callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, {}, unknown>, + policy: string, + body: unknown +): Promise => { + return callWithRequest('transport.request', { + path: `_ilm/policy/${policy}`, + method: 'PUT', + body, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts new file mode 100644 index 0000000000000..a679a61e10c00 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts @@ -0,0 +1,20 @@ +/* + * 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 { IndicesPutTemplateParams } from 'elasticsearch'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +import { CallWithRequest } from './types'; + +export const setTemplate = async ( + callWithRequest: CallWithRequest, + name: string, + body: unknown +): Promise => { + return callWithRequest('indices.putTemplate', { + name, + body, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/types.ts new file mode 100644 index 0000000000000..70b33ad274ee1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/types.ts @@ -0,0 +1,7 @@ +/* + * 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 type CallWithRequest = (endpoint: string, params: T, options?: U) => Promise; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts index c02af2c841a30..3c19ff6f1bb65 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts @@ -8,6 +8,7 @@ import Hapi from 'hapi'; import { KibanaConfig } from 'src/legacy/server/kbn_server'; import { alertsClientMock } from '../../../../../../alerting/server/alerts_client.mock'; import { actionsClientMock } from '../../../../../../actions/server/actions_client.mock'; +import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; const defaultConfig = { 'kibana.index': '.kibana', @@ -46,11 +47,17 @@ export const createMockServer = (config: Record = defaultConfig) const actionsClient = actionsClientMock.create(); const alertsClient = alertsClientMock.create(); + const elasticsearch = { + getCluster: jest.fn().mockImplementation(() => ({ + callWithRequest: jest.fn(), + })), + }; server.decorate('request', 'getAlertsClient', () => alertsClient); server.decorate('request', 'getBasePath', () => '/s/default'); server.decorate('request', 'getActionsClient', () => actionsClient); + server.plugins.elasticsearch = (elasticsearch as unknown) as ElasticsearchPlugin; - return { server, alertsClient, actionsClient }; + return { server, alertsClient, actionsClient, elasticsearch }; }; export const createMockServerWithoutAlertClientDecoration = ( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts index 4c222c196300c..4aa57f005445b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts @@ -22,11 +22,15 @@ import { import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; describe('create_rules', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient } = createMockServer()); + ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); + elasticsearch.getCluster = jest.fn().mockImplementation(() => ({ + callWithRequest: jest.fn().mockImplementation(() => true), + })); + createRulesRoute(server); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts index 2b69e57f2c2ee..31068dac5d23a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts @@ -14,97 +14,112 @@ import { RulesRequest } from '../alerts/types'; import { createRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { readRules } from '../alerts/read_rules'; -import { transformOrError, transformError } from './utils'; +import { transformOrError, transformError, getIndex, callWithRequestFactory } from './utils'; +import { getIndexExists } from '../index/get_index_exists'; -export const createCreateRulesRoute: Hapi.ServerRoute = { - method: 'POST', - path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:siem'], - validate: { - options: { - abortEarly: false, +export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'POST', + path: DETECTION_ENGINE_RULES_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: createRulesSchema, }, - payload: createRulesSchema, }, - }, - async handler(request: RulesRequest, headers) { - const { - description, - enabled, - false_positives: falsePositives, - filter, - from, - immutable, - query, - language, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threats, - to, - type, - references, - } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { - return headers.response().code(404); - } - - try { - if (ruleId != null) { - const rule = await readRules({ alertsClient, ruleId }); - if (rule != null) { - return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); - } - } - - const createdRule = await createRules({ - alertsClient, - actionsClient, + async handler(request: RulesRequest, headers) { + const { description, enabled, - falsePositives, + false_positives: falsePositives, filter, from, immutable, query, language, - outputIndex, - savedId, + output_index: outputIndex, + saved_id: savedId, meta, filters, - ruleId: ruleId != null ? ruleId : uuid.v4(), + rule_id: ruleId, index, interval, - maxSignals, - riskScore, + max_signals: maxSignals, + risk_score: riskScore, name, severity, tags, + threats, to, type, - threats, references, - }); - return transformOrError(createdRule); - } catch (err) { - return transformError(err); - } - }, + } = request.payload; + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + + try { + const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const callWithRequest = callWithRequestFactory(request); + const indexExists = await getIndexExists(callWithRequest, finalIndex); + if (!indexExists) { + return new Boom( + `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + { + statusCode: 400, + } + ); + } + if (ruleId != null) { + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); + } + } + const createdRule = await createRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + filter, + from, + immutable, + query, + language, + outputIndex: finalIndex, + savedId, + meta, + filters, + ruleId: ruleId != null ? ruleId : uuid.v4(), + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threats, + references, + }); + return transformOrError(createdRule); + } catch (err) { + return transformError(err); + } + }, + }; }; export const createRulesRoute = (server: ServerFacade) => { - server.route(createCreateRulesRoute); + server.route(createCreateRulesRoute(server)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts new file mode 100644 index 0000000000000..14a061fd4ccb7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -0,0 +1,63 @@ +/* + * 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 Hapi from 'hapi'; +import Boom from 'boom'; + +import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; +import signalsPolicy from './signals_policy.json'; +import { ServerFacade, RequestFacade } from '../../../../types'; +import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { getIndexExists } from '../../index/get_index_exists'; +import { getPolicyExists } from '../../index/get_policy_exists'; +import { setPolicy } from '../../index/set_policy'; +import { setTemplate } from '../../index/set_template'; +import { getSignalsTemplate } from './get_signals_template'; +import { getTemplateExists } from '../../index/get_template_exists'; +import { createBootstrapIndex } from '../../index/create_bootstrap_index'; + +export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'POST', + path: DETECTION_ENGINE_INDEX_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + }, + }, + async handler(request: RequestFacade) { + try { + const index = getIndex(request, server); + const callWithRequest = callWithRequestFactory(request); + const indexExists = await getIndexExists(callWithRequest, index); + if (indexExists) { + return new Boom(`index ${index} already exists`, { statusCode: 409 }); + } else { + const policyExists = await getPolicyExists(callWithRequest, index); + if (!policyExists) { + await setPolicy(callWithRequest, index, signalsPolicy); + } + const templateExists = await getTemplateExists(callWithRequest, index); + if (!templateExists) { + const template = getSignalsTemplate(index); + await setTemplate(callWithRequest, index, template); + } + createBootstrapIndex(callWithRequest, index); + return { acknowledged: true }; + } + } catch (err) { + return transformError(err); + } + }, + }; +}; + +export const createIndexRoute = (server: ServerFacade) => { + server.route(createCreateIndexRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts new file mode 100644 index 0000000000000..89f21bbada939 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -0,0 +1,70 @@ +/* + * 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 Hapi from 'hapi'; +import Boom from 'boom'; + +import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; +import { ServerFacade, RequestFacade } from '../../../../types'; +import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { getIndexExists } from '../../index/get_index_exists'; +import { getPolicyExists } from '../../index/get_policy_exists'; +import { deletePolicy } from '../../index/delete_policy'; +import { getTemplateExists } from '../../index/get_template_exists'; +import { deleteAllIndex } from '../../index/delete_all_index'; +import { deleteTemplate } from '../../index/delete_template'; + +/** + * Deletes all of the indexes, template, ilm policies, and aliases. You can check + * this by looking at each of these settings from ES after a deletion: + * GET /_template/.siem-signals-default + * GET /.siem-signals-default-000001/ + * GET /_ilm/policy/.signals-default + * GET /_alias/.siem-signals-default + * + * And ensuring they're all gone + */ +export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'DELETE', + path: DETECTION_ENGINE_INDEX_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + }, + }, + async handler(request: RequestFacade) { + try { + const index = getIndex(request, server); + const callWithRequest = callWithRequestFactory(request); + const indexExists = await getIndexExists(callWithRequest, index); + if (!indexExists) { + return new Boom(`index ${index} does not exist`, { statusCode: 404 }); + } else { + await deleteAllIndex(callWithRequest, `${index}-*`); + const policyExists = await getPolicyExists(callWithRequest, index); + if (policyExists) { + await deletePolicy(callWithRequest, index); + } + const templateExists = await getTemplateExists(callWithRequest, index); + if (templateExists) { + await deleteTemplate(callWithRequest, index); + } + return { acknowledged: true }; + } + } catch (err) { + return transformError(err); + } + }, + }; +}; + +export const deleteIndexRoute = (server: ServerFacade) => { + server.route(createDeleteIndexRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/ecs_mapping.json similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/ecs_mapping.json index 94f251645d3fd..06edf94484af3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/ecs_mapping.json @@ -5,186 +5,6 @@ "@timestamp": { "type": "date" }, - "signal": { - "properties": { - "parent": { - "properties": { - "index": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "depth": { - "type": "long" - } - } - }, - "rule": { - "properties": { - "id": { - "type": "keyword" - }, - "rule_id": { - "type": "keyword" - }, - "false_positives": { - "type": "keyword" - }, - "saved_id": { - "type": "keyword" - }, - "max_signals": { - "type": "keyword" - }, - "risk_score": { - "type": "keyword" - }, - "output_index": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "filter": { - "type": "object" - }, - "from": { - "type": "keyword" - }, - "immutable": { - "type": "keyword" - }, - "index": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "language": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "query": { - "type": "keyword" - }, - "references": { - "type": "keyword" - }, - "severity": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "threats": { - "type": "object" - }, - "type": { - "type": "keyword" - }, - "size": { - "type": "keyword" - }, - "to": { - "type": "keyword" - }, - "enabled": { - "type": "keyword" - }, - "filters": { - "type": "object" - }, - "created_by": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "original_time": { - "type": "date" - }, - "original_event": { - "properties": { - "action": { - "type": "keyword" - }, - "category": { - "type": "keyword" - }, - "code": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "dataset": { - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { - "type": "date" - }, - "hash": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "kind": { - "type": "keyword" - }, - "module": { - "type": "keyword" - }, - "original": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "outcome": { - "type": "keyword" - }, - "provider": { - "type": "keyword" - }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "sequence": { - "type": "long" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "status": { - "type": "keyword" - } - } - }, "agent": { "properties": { "ephemeral_id": { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts new file mode 100644 index 0000000000000..80594ca74a353 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts @@ -0,0 +1,31 @@ +/* + * 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 { getSignalsTemplate } from './get_signals_template'; + +describe('get_signals_template', () => { + test('it should set the lifecycle name and the rollover alias to be the name of the index passed in', () => { + const template = getSignalsTemplate('test-index'); + expect(template.settings).toEqual({ + index: { lifecycle: { name: 'test-index', rollover_alias: 'test-index' } }, + }); + }); + + test('it should set have the index patterns with an ending glob in it', () => { + const template = getSignalsTemplate('test-index'); + expect(template.index_patterns).toEqual(['test-index-*']); + }); + + test('it should have a mappings section which is an object type', () => { + const template = getSignalsTemplate('test-index'); + expect(typeof template.mappings).toEqual('object'); + }); + + test('it should have a signals section which is an object type', () => { + const template = getSignalsTemplate('test-index'); + expect(typeof template.mappings.properties.signal).toEqual('object'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts new file mode 100644 index 0000000000000..c6580f0bdda42 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -0,0 +1,25 @@ +/* + * 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 signalsMapping from './signals_mapping.json'; +import ecsMapping from './ecs_mapping.json'; + +export const getSignalsTemplate = (index: string) => { + ecsMapping.mappings.properties.signal = signalsMapping.mappings.properties.signal; + const template = { + settings: { + index: { + lifecycle: { + name: index, + rollover_alias: index, + }, + }, + }, + index_patterns: [`${index}-*`], + mappings: ecsMapping.mappings, + }; + return template; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts new file mode 100644 index 0000000000000..87b6ffb985c37 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -0,0 +1,57 @@ +/* + * 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 Hapi from 'hapi'; +import Boom from 'boom'; + +import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; +import { ServerFacade, RequestFacade } from '../../../../types'; +import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { getIndexExists } from '../../index/get_index_exists'; + +export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'GET', + path: DETECTION_ENGINE_INDEX_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + }, + }, + async handler(request: RequestFacade, headers) { + try { + const index = getIndex(request, server); + const callWithRequest = callWithRequestFactory(request); + const indexExists = await getIndexExists(callWithRequest, index); + if (indexExists) { + // head request is used for if you want to get if the index exists + // or not and it will return a content-length: 0 along with either a 200 or 404 + // depending on if the index exists or not. + if (request.method.toLowerCase() === 'head') { + return headers.response().code(200); + } else { + return headers.response({ name: index }).code(200); + } + } else { + if (request.method.toLowerCase() === 'head') { + return headers.response().code(404); + } else { + return new Boom('An index for this space does not exist', { statusCode: 404 }); + } + } + } catch (err) { + return transformError(err); + } + }, + }; +}; + +export const readIndexRoute = (server: ServerFacade) => { + server.route(createReadIndexRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json new file mode 100644 index 0000000000000..501522105bdbc --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json @@ -0,0 +1,186 @@ +{ + "mappings": { + "properties": { + "signal": { + "properties": { + "parent": { + "properties": { + "index": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "depth": { + "type": "long" + } + } + }, + "rule": { + "properties": { + "id": { + "type": "keyword" + }, + "rule_id": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "risk_score": { + "type": "keyword" + }, + "output_index": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "filter": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threats": { + "type": "object" + }, + "type": { + "type": "keyword" + }, + "size": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "created_by": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "original_time": { + "type": "date" + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + } + } + } + } + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json new file mode 100644 index 0000000000000..640d8e14190cd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_policy.json @@ -0,0 +1,15 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "10gb", + "max_age": "7d" + } + } + } + } + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 40a33e9d97a18..4c5a5a6af93de 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -6,7 +6,9 @@ import Boom from 'boom'; import { pickBy } from 'lodash/fp'; +import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; import { RuleAlertType, isAlertType, OutputRuleAlertRest, isAlertTypes } from '../alerts/types'; +import { ServerFacade, RequestFacade } from '../../../types'; export const getIdError = ({ id, @@ -88,3 +90,16 @@ export const transformError = (err: Error & { statusCode?: number }) => { } } }; + +export const getIndex = (request: RequestFacade, server: ServerFacade): string => { + const spaceId = server.plugins.spaces.getSpaceId(request); + const signalsIndex = server.config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); + return `${signalsIndex}-${spaceId}`; +}; + +export const callWithRequestFactory = (request: RequestFacade) => { + const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data'); + return (endpoint: string, params: T, options?: U) => { + return callWithRequest(request, endpoint, params, options); + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh index c2406dc7f6231..fb3bbbe0fad18 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/check_env_variables.sh @@ -30,11 +30,6 @@ if [ -z "${KIBANA_URL}" ]; then exit 1 fi -if [ -z "${SIGNALS_INDEX}" ]; then - echo "Set SIGNALS_INDEX in your environment" - exit 1 -fi - if [ -z "${TASK_MANAGER_INDEX}" ]; then echo "Set TASK_MANAGER_INDEX in your environment" exit 1 diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh index 8d5deec1ba3a1..aa8f90f27d6d8 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_index.sh @@ -10,9 +10,7 @@ set -e ./check_env_variables.sh # Example: ./delete_signal_index.sh -# https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html curl -s -k \ - -H "Content-Type: application/json" \ + -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${ELASTICSEARCH_URL}/${SIGNALS_INDEX} \ - | jq . + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/put_signal_index.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_index.sh similarity index 56% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/put_signal_index.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_index.sh index 1b3b148a99161..882451631c9eb 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/put_signal_index.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_index.sh @@ -9,10 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./put_signal_index.sh +# Example: ./get_signal_index.sh curl -s -k \ - -H "Content-Type: application/json" \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -d @../signals_mapping.json \ - -X PUT ${ELASTICSEARCH_URL}/${SIGNALS_INDEX} \ - | jq . + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh index ee8fa18e1234d..3cd0bff7ee8ab 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh @@ -13,4 +13,4 @@ set -e ./delete_all_alerts.sh ./delete_all_alert_tasks.sh ./delete_signal_index.sh -./put_signal_index.sh +./post_signal_index.sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_mapping.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh similarity index 53% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_mapping.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh index 8b384fcc76f72..e408f21888c5f 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_mapping.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal_index.sh @@ -9,9 +9,9 @@ set -e ./check_env_variables.sh -# Example: ./get_signal_mapping.sh -# https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html +# Example: ./post_signal_index.sh curl -s -k \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${ELASTICSEARCH_URL}/${SIGNALS_INDEX}/_mapping \ - | jq . + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json index c136c9b0fe808..75a04b5426550 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json @@ -9,7 +9,6 @@ "type": "query", "from": "now-24h", "to": "now", - "output_index": ".siem-signals", "language": "lucene", "query": "", "filters": [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json index 5b69fced90daf..2417a000f9dce 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json @@ -9,7 +9,6 @@ "type": "query", "from": "now-24h", "to": "now", - "output_index": ".siem-signals", "language": "lucene", "filters": [ { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh new file mode 100755 index 0000000000000..b4a494a102b54 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./signal_index_exists.sh +curl -s -k --head \ + -H 'Content-Type: application/json' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts index d6fea820b2698..ad19872b7a75d 100644 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ b/x-pack/legacy/plugins/siem/server/types.ts @@ -13,6 +13,7 @@ export interface ServerFacade { injectUiAppVars: Legacy.Server['injectUiAppVars']; plugins: { alerting?: Legacy.Server['plugins']['alerting']; + spaces: Legacy.Server['plugins']['spaces']; xpack_main: Legacy.Server['plugins']['xpack_main']; }; route: Legacy.Server['route']; From 6db76a7e6df874dbb904126652a472f3612532e6 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 6 Dec 2019 08:23:48 +0100 Subject: [PATCH 05/35] add codeowners for legacy server folder (#52158) --- .github/CODEOWNERS | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 610681e83798e..c5e6768c17d46 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,7 @@ # App /x-pack/legacy/plugins/lens/ @elastic/kibana-app /x-pack/legacy/plugins/graph/ @elastic/kibana-app +/src/legacy/server/sample_data/ @elastic/kibana-app # App Architecture /src/plugins/data/ @elastic/kibana-app-arch @@ -66,14 +67,25 @@ /packages/kbn-es/ @elastic/kibana-operations /packages/kbn-pm/ @elastic/kibana-operations /packages/kbn-test/ @elastic/kibana-operations +/src/legacy/server/keystore/ @elastic/kibana-operations +/src/legacy/server/pid/ @elastic/kibana-operations +/src/legacy/server/sass/ @elastic/kibana-operations +/src/legacy/server/utils/ @elastic/kibana-operations +/src/legacy/server/warnings/ @elastic/kibana-operations # Platform /src/core/ @elastic/kibana-platform -/src/legacy/server/saved_objects/ @elastic/kibana-platform /config/kibana.yml @elastic/kibana-platform /x-pack/plugins/features/ @elastic/kibana-platform /x-pack/plugins/licensing/ @elastic/kibana-platform /packages/kbn-config-schema/ @elastic/kibana-platform +/src/legacy/server/config/ @elastic/kibana-platform +/src/legacy/server/csp/ @elastic/kibana-platform +/src/legacy/server/http/ @elastic/kibana-platform +/src/legacy/server/i18n/ @elastic/kibana-platform +/src/legacy/server/logging/ @elastic/kibana-platform +/src/legacy/server/saved_objects/ @elastic/kibana-platform +/src/legacy/server/status/ @elastic/kibana-platform # Security /x-pack/legacy/plugins/security/ @elastic/kibana-security From ca554024966ecca9a461fe53a61042317816dee0 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 6 Dec 2019 10:28:29 +0100 Subject: [PATCH 06/35] make defaultRoute accessible in NP Config (#52308) * defaultRoute was not provided to the NP * improve defaultRoute validation * add test that defaultRoute is read from config * update tests --- src/core/server/http/http_config.ts | 10 +++- ...gacy_object_to_config_adapter.test.ts.snap | 2 + .../config/legacy_object_to_config_adapter.ts | 1 + .../default_route_provider_config.test.ts | 53 +++++++++++++++++++ .../saved_objects/saved_objects_mixin.js | 2 +- 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/legacy/server/http/integration_tests/default_route_provider_config.test.ts diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 89676380610a9..cb7726de4da5a 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -38,7 +38,15 @@ export const config = { validate: match(validBasePathRegex, "must start with a slash, don't end with one"), }) ), - defaultRoute: schema.maybe(schema.string()), + defaultRoute: schema.maybe( + schema.string({ + validate(value) { + if (!value.startsWith('/')) { + return 'must start with a slash'; + } + }, + }) + ), cors: schema.conditional( schema.contextRef('dev'), true, diff --git a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 172feec674677..d327860052eb9 100644 --- a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -8,6 +8,7 @@ Object { "enabled": true, }, "cors": false, + "defaultRoute": undefined, "host": "host", "keepaliveTimeout": 5000, "maxPayload": 1000, @@ -30,6 +31,7 @@ Object { "enabled": true, }, "cors": false, + "defaultRoute": undefined, "host": "host", "keepaliveTimeout": 5000, "maxPayload": 1000, diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts index 6f0757dece165..8035596bb6072 100644 --- a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts +++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts @@ -62,6 +62,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { return { autoListen: configValue.autoListen, basePath: configValue.basePath, + defaultRoute: configValue.defaultRoute, cors: configValue.cors, host: configValue.host, maxPayload: configValue.maxPayloadBytes, diff --git a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts b/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts new file mode 100644 index 0000000000000..da785a59893ab --- /dev/null +++ b/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts @@ -0,0 +1,53 @@ +/* + * 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 * as kbnTestServer from '../../../../test_utils/kbn_server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Root } from '../../../../core/server/root'; + +describe('default route provider', () => { + let root: Root; + + afterEach(async () => await root.shutdown()); + + it('redirects to the configured default route', async function() { + root = kbnTestServer.createRoot({ + server: { + defaultRoute: '/app/some/default/route', + }, + }); + + await root.setup(); + await root.start(); + + const kbnServer = kbnTestServer.getKbnServer(root); + + kbnServer.server.decorate('request', 'getSavedObjectsClient', function() { + return { + get: (type: string, id: string) => ({ attributes: {} }), + }; + }); + + const { status, header } = await kbnTestServer.request.get(root, '/'); + + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/app/some/default/route', + }); + }); +}); diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index 7324a02095c67..e0d8b895af838 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -55,7 +55,7 @@ function getImportableAndExportableTypes({ kbnServer, visibleTypes }) { ); } -export async function savedObjectsMixin(kbnServer, server) { +export function savedObjectsMixin(kbnServer, server) { const migrator = kbnServer.newPlatform.__internals.kibanaMigrator; const mappings = migrator.getActiveMappings(); const allTypes = Object.keys(getRootPropertiesObjects(mappings)); From 881c836f9489a122cb1d1e2e0d32b4d9c2289f91 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 6 Dec 2019 13:20:29 +0100 Subject: [PATCH 07/35] [State Management] Move url state_hashing utils to kibana_utils (#52280) Part of #44151, Continuation of #51835, Just moves existing state related url utils to kibana_utils plugin Also fixes small regression introduced in #51835, When sharing hashed url directly it should show error toast instead of full page fatal error --- .i18nrc.json | 1 + .../kibana/public/dashboard/legacy_imports.ts | 2 +- .../kibana/public/discover/kibana_services.ts | 2 +- .../kibana/public/visualize/editor/editor.js | 2 +- .../public/visualize/kibana_services.ts | 2 +- .../public/index.js | 2 +- .../ui/public/chrome/api/sub_url_hooks.js | 15 ++++--- .../state_management/__tests__/state.js | 6 +-- .../ui/public/state_management/state.js | 6 +-- src/plugins/kibana_utils/public/index.ts | 2 + .../state_management/state_hash}/index.ts | 3 +- .../state_hash}/state_hash.test.ts | 5 +-- .../state_hash}/state_hash.ts | 2 +- .../url}/hash_unhash_url.test.ts | 5 +-- .../state_management/url}/hash_unhash_url.ts | 8 ++-- .../public/state_management/url/index.ts | 20 ++++++++++ test/typings/encode_uri_query.d.ts | 24 ++++++++++++ test/typings/rison_node.d.ts | 39 +++++++++++++++++++ .../translations/translations/ja-JP.json | 4 +- .../translations/translations/zh-CN.json | 2 + 20 files changed, 120 insertions(+), 32 deletions(-) rename src/{legacy/ui/public/state_management/state_hashing => plugins/kibana_utils/public/state_management/state_hash}/index.ts (85%) rename src/{legacy/ui/public/state_management/state_hashing => plugins/kibana_utils/public/state_management/state_hash}/state_hash.test.ts (91%) rename src/{legacy/ui/public/state_management/state_hashing => plugins/kibana_utils/public/state_management/state_hash}/state_hash.ts (96%) rename src/{legacy/ui/public/state_management/state_hashing => plugins/kibana_utils/public/state_management/url}/hash_unhash_url.test.ts (97%) rename src/{legacy/ui/public/state_management/state_hashing => plugins/kibana_utils/public/state_management/url}/hash_unhash_url.ts (94%) create mode 100644 src/plugins/kibana_utils/public/state_management/url/index.ts create mode 100644 test/typings/encode_uri_query.d.ts create mode 100644 test/typings/rison_node.d.ts diff --git a/.i18nrc.json b/.i18nrc.json index e5ba6762da154..fac9b9ce53184 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -19,6 +19,7 @@ "kbnVislibVisTypes": "src/legacy/core_plugins/kbn_vislib_vis_types", "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", + "kibana_utils": "src/plugins/kibana_utils", "navigation": "src/legacy/core_plugins/navigation", "newsfeed": "src/plugins/newsfeed", "regionMap": "src/legacy/core_plugins/region_map", diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index 7c3c389330887..b0f09f0cf9745 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -63,6 +63,6 @@ export { confirmModalFactory } from 'ui/modals/confirm_modal'; export { configureAppAngularModule } from 'ui/legacy_compat'; export { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory'; export { ensureDefaultIndexPattern } from 'ui/legacy_compat'; -export { unhashUrl } from 'ui/state_management/state_hashing'; +export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; export { IInjector } from 'ui/chrome'; export { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 0d9dab96d6120..43a0afa83dfe4 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -78,7 +78,7 @@ export { tabifyAggResponse } from 'ui/agg_response/tabify'; // @ts-ignore export { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; export { ensureDefaultIndexPattern } from 'ui/legacy_compat'; -export { unhashUrl } from 'ui/state_management/state_hashing'; +export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; // EXPORT types export { Vis } from 'ui/vis'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index 2cf2584810741..c985e1a00655f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -45,7 +45,7 @@ import { showSaveModal, stateMonitorFactory, subscribeWithScope, - unhashUrl, + unhashUrl } from '../kibana_services'; const { diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 6477d1941c205..40d36dab227fa 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -103,7 +103,7 @@ export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; -export { unhashUrl } from 'ui/state_management/state_hashing'; +export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; export { Container, Embeddable, diff --git a/src/legacy/core_plugins/state_session_storage_redirect/public/index.js b/src/legacy/core_plugins/state_session_storage_redirect/public/index.js index 1aa7bce2af699..7fbf715ac3616 100644 --- a/src/legacy/core_plugins/state_session_storage_redirect/public/index.js +++ b/src/legacy/core_plugins/state_session_storage_redirect/public/index.js @@ -18,7 +18,7 @@ */ import chrome from 'ui/chrome'; -import { hashUrl } from 'ui/state_management/state_hashing'; +import { hashUrl } from '../../../../plugins/kibana_utils/public'; import uiRoutes from 'ui/routes'; import { fatalError } from 'ui/notify'; diff --git a/src/legacy/ui/public/chrome/api/sub_url_hooks.js b/src/legacy/ui/public/chrome/api/sub_url_hooks.js index e38a1f4b19e56..3ff262f546e3c 100644 --- a/src/legacy/ui/public/chrome/api/sub_url_hooks.js +++ b/src/legacy/ui/public/chrome/api/sub_url_hooks.js @@ -19,9 +19,8 @@ import url from 'url'; -import { - unhashUrl, -} from '../../state_management/state_hashing'; +import { unhashUrl } from '../../../../../plugins/kibana_utils/public'; +import { toastNotifications } from '../../notify/toasts'; export function registerSubUrlHooks(angularModule, internals) { angularModule.run(($rootScope, Private, $location) => { @@ -29,8 +28,14 @@ export function registerSubUrlHooks(angularModule, internals) { function updateSubUrls() { const urlWithHashes = window.location.href; - const urlWithStates = unhashUrl(urlWithHashes); - internals.trackPossibleSubUrl(urlWithStates); + let urlWithStates; + try { + urlWithStates = unhashUrl(urlWithHashes); + } catch (e) { + toastNotifications.addDanger(e.message); + } + + internals.trackPossibleSubUrl(urlWithStates || urlWithHashes); } function onRouteChange($event) { diff --git a/src/legacy/ui/public/state_management/__tests__/state.js b/src/legacy/ui/public/state_management/__tests__/state.js index 6f6f74c9d2bec..475d7c44a5f5a 100644 --- a/src/legacy/ui/public/state_management/__tests__/state.js +++ b/src/legacy/ui/public/state_management/__tests__/state.js @@ -26,11 +26,11 @@ import { toastNotifications } from '../../notify'; import * as FatalErrorNS from '../../notify/fatal_error'; import { StateProvider } from '../state'; import { + unhashQuery, createStateHash, isStateHash, - unhashQuery -} from '../state_hashing'; -import { HashedItemStore } from '../../../../../plugins/kibana_utils/public'; + HashedItemStore +} from '../../../../../plugins/kibana_utils/public'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { EventsProvider } from '../../events'; diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js index 359dfa5749611..27186b4249978 100644 --- a/src/legacy/ui/public/state_management/state.js +++ b/src/legacy/ui/public/state_management/state.js @@ -35,11 +35,7 @@ import { fatalError, toastNotifications } from '../notify'; import './config_provider'; import { createLegacyClass } from '../utils/legacy_class'; import { callEach } from '../utils/function'; -import { hashedItemStore } from '../../../../plugins/kibana_utils/public'; -import { - createStateHash, - isStateHash -} from './state_hashing'; +import { hashedItemStore, isStateHash, createStateHash } from '../../../../plugins/kibana_utils/public'; export function StateProvider(Private, $rootScope, $location, stateManagementConfig, config, kbnUrl, $injector) { const Events = Private(EventsProvider); diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 22ac720246d4b..c5c129eca8fd3 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -28,3 +28,5 @@ export * from './errors'; export * from './field_mapping'; export * from './storage'; export * from './storage/hashed_item_store'; +export * from './state_management/state_hash'; +export * from './state_management/url'; diff --git a/src/legacy/ui/public/state_management/state_hashing/index.ts b/src/plugins/kibana_utils/public/state_management/state_hash/index.ts similarity index 85% rename from src/legacy/ui/public/state_management/state_hashing/index.ts rename to src/plugins/kibana_utils/public/state_management/state_hash/index.ts index 6225202f90978..0e52c4c55872d 100644 --- a/src/legacy/ui/public/state_management/state_hashing/index.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export { hashUrl, unhashUrl, hashQuery, unhashQuery } from './hash_unhash_url'; -export { createStateHash, isStateHash } from './state_hash'; +export * from './state_hash'; diff --git a/src/legacy/ui/public/state_management/state_hashing/state_hash.test.ts b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts similarity index 91% rename from src/legacy/ui/public/state_management/state_hashing/state_hash.test.ts rename to src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts index 83a94e37785c4..cccb74acaf1e5 100644 --- a/src/legacy/ui/public/state_management/state_hashing/state_hash.test.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.test.ts @@ -18,9 +18,8 @@ */ import { encode as encodeRison } from 'rison-node'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { mockStorage } from '../../../../../plugins/kibana_utils/public/storage/hashed_item_store/mock'; -import { createStateHash, isStateHash } from '../state_hashing'; +import { mockStorage } from '../../storage/hashed_item_store/mock'; +import { createStateHash, isStateHash } from './state_hash'; describe('stateHash', () => { beforeEach(() => { diff --git a/src/legacy/ui/public/state_management/state_hashing/state_hash.ts b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts similarity index 96% rename from src/legacy/ui/public/state_management/state_hashing/state_hash.ts rename to src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts index b3574876bafae..a3eb5272b112d 100644 --- a/src/legacy/ui/public/state_management/state_hashing/state_hash.ts +++ b/src/plugins/kibana_utils/public/state_management/state_hash/state_hash.ts @@ -18,7 +18,7 @@ */ import { Sha256 } from '../../../../../core/public/utils'; -import { hashedItemStore } from '../../../../../plugins/kibana_utils/public'; +import { hashedItemStore } from '../../storage/hashed_item_store'; // This prefix is used to identify hash strings that have been encoded in the URL. const HASH_PREFIX = 'h@'; diff --git a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.test.ts b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts similarity index 97% rename from src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.test.ts rename to src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts index afbe86a4b4d12..a85158acddefd 100644 --- a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.test.ts @@ -17,9 +17,8 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { mockStorage } from '../../../../../plugins/kibana_utils/public/storage/hashed_item_store/mock'; -import { HashedItemStore } from '../../../../../plugins/kibana_utils/public'; +import { mockStorage } from '../../storage/hashed_item_store/mock'; +import { HashedItemStore } from '../../storage/hashed_item_store'; import { hashUrl, unhashUrl } from './hash_unhash_url'; describe('hash unhash url', () => { diff --git a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.ts b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts similarity index 94% rename from src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.ts rename to src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts index 7142683c25115..872e7953f938b 100644 --- a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.ts +++ b/src/plugins/kibana_utils/public/state_management/url/hash_unhash_url.ts @@ -22,8 +22,8 @@ import rison, { RisonObject } from 'rison-node'; import { stringify as stringifyQueryString } from 'querystring'; import encodeUriQuery from 'encode-uri-query'; import { format as formatUrl, parse as parseUrl } from 'url'; -import { hashedItemStore } from '../../../../../plugins/kibana_utils/public'; -import { createStateHash, isStateHash } from './state_hash'; +import { hashedItemStore } from '../../storage/hashed_item_store'; +import { createStateHash, isStateHash } from '../state_hash'; export type IParsedUrlQuery = Record; @@ -98,7 +98,7 @@ export function retrieveState(stateHash: string): RisonObject { const json = hashedItemStore.getItem(stateHash); const throwUnableToRestoreUrlError = () => { throw new Error( - i18n.translate('common.ui.stateManagement.unableToRestoreUrlErrorMessage', { + i18n.translate('kibana_utils.stateManagement.url.unableToRestoreUrlErrorMessage', { defaultMessage: 'Unable to completely restore the URL, be sure to use the share functionality.', }) @@ -125,7 +125,7 @@ export function persistState(state: RisonObject): string { if (isItemSet) return hash; // If we ran out of space trying to persist the state, notify the user. const message = i18n.translate( - 'common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage', + 'kibana_utils.stateManagement.url.unableToStoreHistoryInSessionErrorMessage', { defaultMessage: 'Kibana is unable to store history items in your session ' + diff --git a/src/plugins/kibana_utils/public/state_management/url/index.ts b/src/plugins/kibana_utils/public/state_management/url/index.ts new file mode 100644 index 0000000000000..30c5696233db7 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/index.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 * from './hash_unhash_url'; diff --git a/test/typings/encode_uri_query.d.ts b/test/typings/encode_uri_query.d.ts new file mode 100644 index 0000000000000..4bfc554624446 --- /dev/null +++ b/test/typings/encode_uri_query.d.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +declare module 'encode-uri-query' { + function encodeUriQuery(query: string, usePercentageSpace?: boolean): string; + // eslint-disable-next-line import/no-default-export + export default encodeUriQuery; +} diff --git a/test/typings/rison_node.d.ts b/test/typings/rison_node.d.ts new file mode 100644 index 0000000000000..2592c36e8ae9a --- /dev/null +++ b/test/typings/rison_node.d.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ + +declare module 'rison-node' { + export type RisonValue = null | boolean | number | string | RisonObject | RisonArray; + + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface RisonArray extends Array {} + + export interface RisonObject { + [key: string]: RisonValue; + } + + export const decode: (input: string) => RisonValue; + + // eslint-disable-next-line @typescript-eslint/camelcase + export const decode_object: (input: string) => RisonObject; + + export const encode: (input: Input) => string; + + // eslint-disable-next-line @typescript-eslint/camelcase + export const encode_object: (input: Input) => string; +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c24f04697e3e5..b9fc90f947e08 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -676,6 +676,8 @@ "kibana-react.savedObjects.saveModal.saveButtonLabel": "保存", "kibana-react.savedObjects.saveModal.saveTitle": "{objectType} を保存", "kibana-react.savedObjects.saveModal.titleLabel": "タイトル", + "kibana_utils.stateManagement.url.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", + "kibana_utils.stateManagement.url.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", "inspector.closeButton": "インスペクターを閉じる", "inspector.reqTimestampDescription": "リクエストの開始が記録された時刻です", "inspector.reqTimestampKey": "リクエストのタイムスタンプ", @@ -12750,4 +12752,4 @@ "xpack.licensing.check.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", "xpack.licensing.check.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6de1bc2135351..19207007ee714 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -677,6 +677,8 @@ "kibana-react.savedObjects.saveModal.saveButtonLabel": "保存", "kibana-react.savedObjects.saveModal.saveTitle": "保存 {objectType}", "kibana-react.savedObjects.saveModal.titleLabel": "标题", + "kibana_utils.stateManagement.url.unableToRestoreUrlErrorMessage": "无法完整还原 URL,确保使用共享功能。", + "kibana_utils.stateManagement.url.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,并且似乎没有任何可安全删除的项。\n\n通常可通过移至新的标签页来解决此问题,但这会导致更大的问题。如果您有规律地看到此消息,请在 {gitHubIssuesUrl} 提交问题。", "inspector.closeButton": "关闭检查器", "inspector.reqTimestampDescription": "记录请求启动的时间", "inspector.reqTimestampKey": "请求时间戳", From 2a83266ed994085cb1f05daac7b07e12b06eb193 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Fri, 6 Dec 2019 08:27:51 -0700 Subject: [PATCH 08/35] [SIEM] Remove placeholder from pinned event tooltips (#52361) ## [SIEM] Remove placeholder from pinned event tooltips Similar to signals, pinned timeline events should be copied from source indexes, which are subject to ILM, to separate (space-aware) indexes (with different ILM), such that pinned events can be viewed in a timeline after the events have aged out of the original indexes. The backend APIs and UI patterns in development now for signals can likely be reused to implement the above, but until then, the placeholder tooltip text for unpinned / pinned events, which mentions persistence, should be removed from the SIEM beta. - [x] Changed the _unpinned_ event tooltip text from (sic) `This is event is NOT persisted with the timeline` to `Unpinned event` - [x] Changed the pinned event tooltip text from `This event is persisted with the timeline` to `Pinned event` https://github.com/elastic/siem-team/issues/482 --- .../components/timeline/body/helpers.test.ts | 20 +++++++------------ .../components/timeline/body/translations.ts | 4 ++-- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts index 83099c68e4ca9..602b88b7ae6d6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts @@ -203,28 +203,22 @@ describe('helpers', () => { }); describe('getPinTooltip', () => { - test('it informs the user the event may not be unpinned when the event is pinned and has notes', () => { + test('it indicates the event may NOT be unpinned when `isPinned` is `true` and the event has notes', () => { expect(getPinTooltip({ isPinned: true, eventHasNotes: true })).toEqual( 'This event cannot be unpinned because it has notes' ); }); - test('it tells the user the event is persisted when the event is pinned, but has no notes', () => { - expect(getPinTooltip({ isPinned: true, eventHasNotes: false })).toEqual( - 'This event is persisted with the timeline' - ); + test('it indicates the event is pinned when `isPinned` is `true` and the event does NOT have notes', () => { + expect(getPinTooltip({ isPinned: true, eventHasNotes: false })).toEqual('Pinned event'); }); - test('it tells the user the event is NOT persisted when the event is not pinned, but it has notes', () => { - expect(getPinTooltip({ isPinned: false, eventHasNotes: true })).toEqual( - 'This is event is NOT persisted with the timeline' - ); + test('it indicates the event is NOT pinned when `isPinned` is `false` and the event has notes', () => { + expect(getPinTooltip({ isPinned: false, eventHasNotes: true })).toEqual('Unpinned event'); }); - test('it tells the user the event is NOT persisted when the event is not pinned, and has no notes', () => { - expect(getPinTooltip({ isPinned: false, eventHasNotes: false })).toEqual( - 'This is event is NOT persisted with the timeline' - ); + test('it indicates the event is NOT pinned when `isPinned` is `false` and the event does NOT have notes', () => { + expect(getPinTooltip({ isPinned: false, eventHasNotes: false })).toEqual('Unpinned event'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/translations.ts index 40e94cb099644..03efc26bbc9b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/translations.ts @@ -21,11 +21,11 @@ export const COPY_TO_CLIPBOARD = i18n.translate( ); export const UNPINNED = i18n.translate('xpack.siem.timeline.body.pinning.unpinnedTooltip', { - defaultMessage: 'This is event is NOT persisted with the timeline', + defaultMessage: 'Unpinned event', }); export const PINNED = i18n.translate('xpack.siem.timeline.body.pinning.pinnedTooltip', { - defaultMessage: 'This event is persisted with the timeline', + defaultMessage: 'Pinned event', }); export const PINNED_WITH_NOTES = i18n.translate( From 80eef1ec0646e1a7a210b42c538994131db5bcd0 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 6 Dec 2019 16:32:41 +0100 Subject: [PATCH 09/35] [ML] Fetch the latest job messages and enable sorting by time (#52388) * [ML] add sorting support * [ML] change fetch sort to desc for anomaly detection jobs * [ML] rename param --- .../components/job_messages/job_messages.tsx | 17 ++++++++++++++--- .../job_audit_messages/job_audit_messages.js | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx index aedb8b6d17d06..5fb3ab95e4ea0 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; -import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; +import { EuiSpacer, EuiInMemoryTable } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; import { i18n } from '@kbn/i18n'; @@ -35,11 +35,13 @@ export const JobMessages: FC = ({ messages, loading, error }) width: `${theme.euiSizeL}`, }, { + field: 'timestamp', name: i18n.translate('xpack.ml.jobMessages.timeLabel', { defaultMessage: 'Time', }), - render: (message: any) => formatDate(message.timestamp, TIME_FORMAT), + render: (timestamp: number) => formatDate(timestamp, TIME_FORMAT), width: '120px', + sortable: true, }, { field: 'node_name', @@ -57,13 +59,22 @@ export const JobMessages: FC = ({ messages, loading, error }) }, ]; + const defaultSorting = { + sort: { + field: 'timestamp', + direction: 'asc', + }, + }; + return ( <> - Date: Fri, 6 Dec 2019 16:37:49 +0100 Subject: [PATCH 10/35] [ML] Functional tests for Additional settings in the Job wizards (#52269) * [ML] test custom urls in multi-metric wizard * [ML] calendars test * [ML] tests for job cloning * [ML] single metric * [ML] advanced job * [ML] population job * [ML] update snapshot * [ML] ensure calendar deleted and created * [ML] improve custom urls assertation * [ML] update snapshot * [ML] update snapshot, fix data-test-subject * [ML] remove redundant functions * [ML] add ensureAdditionalSettingsSectionOpen check * [ML] remove assignCalendar method * [ML] ensure model window disappears after adding a custom url * [ML] create calendar logging, remove unused deleteCalendar method, parameterized saveCustomUrl --- .../__snapshots__/editor.test.tsx.snap | 6 ++ .../__snapshots__/list.test.tsx.snap | 12 +++- .../components/custom_url_editor/editor.tsx | 1 + .../components/custom_url_editor/list.tsx | 5 +- .../edit_job_flyout/tabs/custom_urls.tsx | 19 +++++- .../additional_section/additional_section.tsx | 27 +++++---- .../calendars/calendars_selection.tsx | 2 +- .../anomaly_detection/advanced_job.ts | 25 ++++++++ .../anomaly_detection/multi_metric_job.ts | 25 ++++++++ .../anomaly_detection/population_job.ts | 25 ++++++++ .../anomaly_detection/single_metric_job.ts | 25 ++++++++ .../services/machine_learning/api.ts | 20 +++++++ .../services/machine_learning/custom_urls.ts | 45 ++++++++++++++ .../services/machine_learning/index.ts | 1 + .../machine_learning/job_wizard_common.ts | 58 ++++++++++++++++++- x-pack/test/functional/services/ml.ts | 5 +- 16 files changed, 279 insertions(+), 22 deletions(-) create mode 100644 x-pack/test/functional/services/machine_learning/custom_urls.ts diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap index 6e768cc301852..d98dcb26ee238 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap @@ -40,6 +40,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe > +