diff --git a/.eslintignore b/.eslintignore index 357d735e8044b..1f22b6074e76e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,14 +9,14 @@ bower_components /built_assets /html_docs /src/plugins/data/common/es_query/kuery/ast/_generated_/** -/src/legacy/core_plugins/vis_type_timelion/public/_generated_/** +/src/plugins/vis_type_timelion/public/_generated_/** src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data /src/legacy/ui/public/flot-charts /test/fixtures/scenarios /src/legacy/core_plugins/console/public/webpackShims /src/legacy/core_plugins/console/public/tests/webpackShims /src/legacy/ui/public/utils/decode_geo_hash.js -/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.* +/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.* /src/core/lib/kbn_internal_native_observable /packages/*/target /packages/eslint-config-kibana diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 632f28efdc37e..ab05b32ab063e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,17 +9,18 @@ /src/legacy/server/sample_data/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/discover/ @elastic/kibana-app -/src/legacy/core_plugins/kibana/public/visualize/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app /src/plugins/vis_type_xy/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app -/src/plugins/timelion/ @elastic/kibana-app +/src/plugins/vis_type_timelion/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app +/src/plugins/visualize/ @elastic/kibana-app /src/plugins/vis_type_timeseries/ @elastic/kibana-app /src/plugins/vis_type_metric/ @elastic/kibana-app +/src/plugins/vis_type_markdown/ @elastic/kibana-app # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon @@ -208,9 +209,12 @@ # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team /x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team +/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team /x-pack/test/functional_endpoint/ @elastic/endpoint-app-team /x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team /x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team +/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team # SIEM /x-pack/legacy/plugins/siem/ @elastic/siem diff --git a/.i18nrc.json b/.i18nrc.json index 2edf178e37bd5..4a516f23ebf05 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -43,10 +43,10 @@ "src/plugins/telemetry_management_section" ], "tileMap": "src/legacy/core_plugins/tile_map", - "timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion", "src/plugins/timelion"], + "timelion": ["src/legacy/core_plugins/timelion", "src/plugins/vis_type_timelion"], "uiActions": "src/plugins/ui_actions", "visDefaultEditor": "src/plugins/vis_default_editor", - "visTypeMarkdown": "src/legacy/core_plugins/vis_type_markdown", + "visTypeMarkdown": "src/plugins/vis_type_markdown", "visTypeMetric": "src/plugins/vis_type_metric", "visTypeTable": "src/legacy/core_plugins/vis_type_table", "visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud", @@ -54,7 +54,8 @@ "visTypeVega": "src/legacy/core_plugins/vis_type_vega", "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", "visTypeXy": "src/legacy/core_plugins/vis_type_xy", - "visualizations": "src/plugins/visualizations" + "visualizations": "src/plugins/visualizations", + "visualize": "src/plugins/visualize" }, "exclude": [ "src/legacy/ui/ui_render/ui_render_mixin.js" diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.http.md index dcc1d754feb7c..a8028827cc0a4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.http.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.http.md @@ -9,5 +9,7 @@ Signature: ```typescript -http: HttpServiceSetup; +http: HttpServiceSetup & { + resources: HttpResources; + }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index c10b460da8b4f..30c054345928b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -20,7 +20,7 @@ export interface CoreSetupContextSetup | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | | [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | -| [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | +| [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | | [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | | [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresources.md b/docs/development/core/server/kibana-plugin-core-server.httpresources.md new file mode 100644 index 0000000000000..cb3170e989e17 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresources.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResources](./kibana-plugin-core-server.httpresources.md) + +## HttpResources interface + +HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file. + +Signature: + +```typescript +export interface HttpResources +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [register](./kibana-plugin-core-server.httpresources.register.md) | <P, Q, B>(route: RouteConfig<P, Q, B, 'get'>, handler: HttpResourcesRequestHandler<P, Q, B>) => void | To register a route handler executing passed function to form response. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresources.register.md b/docs/development/core/server/kibana-plugin-core-server.httpresources.register.md new file mode 100644 index 0000000000000..fe3803a6ffe52 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresources.register.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResources](./kibana-plugin-core-server.httpresources.md) > [register](./kibana-plugin-core-server.httpresources.register.md) + +## HttpResources.register property + +To register a route handler executing passed function to form response. + +Signature: + +```typescript +register: (route: RouteConfig, handler: HttpResourcesRequestHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.headers.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.headers.md new file mode 100644 index 0000000000000..bb6dec504ff42 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.headers.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) > [headers](./kibana-plugin-core-server.httpresourcesrenderoptions.headers.md) + +## HttpResourcesRenderOptions.headers property + +HTTP Headers with additional information about response. + +Signature: + +```typescript +headers?: ResponseHeaders; +``` + +## Remarks + +All HTML pages are already pre-configured with `content-security-policy` header that cannot be overridden. + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.md new file mode 100644 index 0000000000000..6563e3c636a99 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrenderoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) + +## HttpResourcesRenderOptions interface + +Allows to configure HTTP response parameters + +Signature: + +```typescript +export interface HttpResourcesRenderOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-core-server.httpresourcesrenderoptions.headers.md) | ResponseHeaders | HTTP Headers with additional information about response. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesrequesthandler.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrequesthandler.md new file mode 100644 index 0000000000000..20f930382955e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesrequesthandler.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesRequestHandler](./kibana-plugin-core-server.httpresourcesrequesthandler.md) + +## HttpResourcesRequestHandler type + +Extended version of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) having access to [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) to respond with HTML or JS resources. + +Signature: + +```typescript +export declare type HttpResourcesRequestHandler

= RequestHandler; +``` + +## Example + +\`\`\`typescript httpResources.register({ path: '/login', validate: { params: schema.object({ id: schema.string() }), }, }, async (context, request, response) => { //.. return response.renderCoreApp(); }); + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesresponseoptions.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesresponseoptions.md new file mode 100644 index 0000000000000..2ea3ea7e58c78 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesresponseoptions.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesResponseOptions](./kibana-plugin-core-server.httpresourcesresponseoptions.md) + +## HttpResourcesResponseOptions type + +HTTP Resources response parameters + +Signature: + +```typescript +export declare type HttpResourcesResponseOptions = HttpResponseOptions; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.md new file mode 100644 index 0000000000000..1c221e13f534f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) + +## HttpResourcesServiceToolkit interface + +Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource. + +Signature: + +```typescript +export interface HttpResourcesServiceToolkit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [renderAnonymousCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md) | (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse> | To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. | +| [renderCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md) | (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse> | To respond with HTML page bootstrapping Kibana application. | +| [renderHtml](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md) | (options: HttpResourcesResponseOptions) => IKibanaResponse | To respond with a custom HTML page. | +| [renderJs](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md) | (options: HttpResourcesResponseOptions) => IKibanaResponse | To respond with a custom JS script file. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md new file mode 100644 index 0000000000000..3dce9d88c8036 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) > [renderAnonymousCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md) + +## HttpResourcesServiceToolkit.renderAnonymousCoreApp property + +To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. + +Signature: + +```typescript +renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md new file mode 100644 index 0000000000000..eb4f095bc19be --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) > [renderCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md) + +## HttpResourcesServiceToolkit.renderCoreApp property + +To respond with HTML page bootstrapping Kibana application. + +Signature: + +```typescript +renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md new file mode 100644 index 0000000000000..325d19625df44 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) > [renderHtml](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md) + +## HttpResourcesServiceToolkit.renderHtml property + +To respond with a custom HTML page. + +Signature: + +```typescript +renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md new file mode 100644 index 0000000000000..f8d4418fc6cba --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) > [renderJs](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md) + +## HttpResourcesServiceToolkit.renderJs property + +To respond with a custom JS script file. + +Signature: + +```typescript +renderJs: (options: HttpResourcesResponseOptions) => IKibanaResponse; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.irouter.handlelegacyerrors.md b/docs/development/core/server/kibana-plugin-core-server.irouter.handlelegacyerrors.md index 94cf3c94187b0..35d109975c83a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.irouter.handlelegacyerrors.md +++ b/docs/development/core/server/kibana-plugin-core-server.irouter.handlelegacyerrors.md @@ -9,5 +9,5 @@ Wrap a router handler to catch and converts legacy boom errors to proper custom Signature: ```typescript -handleLegacyErrors: (handler: RequestHandler) => RequestHandler; +handleLegacyErrors: RequestHandlerWrapper; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.irouter.md b/docs/development/core/server/kibana-plugin-core-server.irouter.md index 073f02f1a4191..4bade638a65a5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.irouter.md +++ b/docs/development/core/server/kibana-plugin-core-server.irouter.md @@ -18,7 +18,7 @@ export interface IRouter | --- | --- | --- | | [delete](./kibana-plugin-core-server.irouter.delete.md) | RouteRegistrar<'delete'> | Register a route handler for DELETE request. | | [get](./kibana-plugin-core-server.irouter.get.md) | RouteRegistrar<'get'> | Register a route handler for GET request. | -| [handleLegacyErrors](./kibana-plugin-core-server.irouter.handlelegacyerrors.md) | <P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | +| [handleLegacyErrors](./kibana-plugin-core-server.irouter.handlelegacyerrors.md) | RequestHandlerWrapper | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | | [patch](./kibana-plugin-core-server.irouter.patch.md) | RouteRegistrar<'patch'> | Register a route handler for PATCH request. | | [post](./kibana-plugin-core-server.irouter.post.md) | RouteRegistrar<'post'> | Register a route handler for POST request. | | [put](./kibana-plugin-core-server.irouter.put.md) | RouteRegistrar<'put'> | Register a route handler for PUT request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.md b/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.md deleted file mode 100644 index 0632b5e5e2297..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedRenderingClient](./kibana-plugin-core-server.iscopedrenderingclient.md) - -## IScopedRenderingClient interface - - -Signature: - -```typescript -export interface IScopedRenderingClient -``` - -## Methods - -| Method | Description | -| --- | --- | -| [render(options)](./kibana-plugin-core-server.iscopedrenderingclient.render.md) | Generate a KibanaResponse which renders an HTML page bootstrapped with the core bundle. Intended as a response body for HTTP route handlers. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.render.md b/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.render.md deleted file mode 100644 index ca114bed21149..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.render.md +++ /dev/null @@ -1,41 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedRenderingClient](./kibana-plugin-core-server.iscopedrenderingclient.md) > [render](./kibana-plugin-core-server.iscopedrenderingclient.render.md) - -## IScopedRenderingClient.render() method - -Generate a `KibanaResponse` which renders an HTML page bootstrapped with the `core` bundle. Intended as a response body for HTTP route handlers. - -Signature: - -```typescript -render(options?: Pick): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| options | Pick<IRenderOptions, 'includeUserSettings'> | | - -Returns: - -`Promise` - -## Example - - -```ts -router.get( - { path: '/', validate: false }, - (context, request, response) => - response.ok({ - body: await context.core.rendering.render(), - headers: { - 'content-security-policy': context.core.http.csp.header, - }, - }) -); - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md index f037b7b3e7cb2..a5c1d59be06d3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md @@ -20,4 +20,5 @@ export interface LegacyServiceSetupDeps | --- | --- | --- | | [core](./kibana-plugin-core-server.legacyservicesetupdeps.core.md) | LegacyCoreSetup | | | [plugins](./kibana-plugin-core-server.legacyservicesetupdeps.plugins.md) | Record<string, unknown> | | +| [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) | UiPlugins | | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md new file mode 100644 index 0000000000000..d19a7dfcbfcfa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) + +## LegacyServiceSetupDeps.uiPlugins property + +Signature: + +```typescript +uiPlugins: UiPlugins; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 5c0f10cac5179..5450e84417f89 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -80,6 +80,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [EnvironmentMode](./kibana-plugin-core-server.environmentmode.md) | | | [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters | | [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | +| [HttpResources](./kibana-plugin-core-server.httpresources.md) | HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file. | +| [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) | Allows to configure HTTP response parameters | +| [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) | Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource. | | [HttpResponseOptions](./kibana-plugin-core-server.httpresponseoptions.md) | HTTP response parameters | | [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | | | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | @@ -92,7 +95,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IndexSettingsDeprecationInfo](./kibana-plugin-core-server.indexsettingsdeprecationinfo.md) | | | [IRenderOptions](./kibana-plugin-core-server.irenderoptions.md) | | | [IRouter](./kibana-plugin-core-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-core-server.routeconfig.md) and [RequestHandler](./kibana-plugin-core-server.requesthandler.md) for more information about arguments to route registrations. | -| [IScopedRenderingClient](./kibana-plugin-core-server.iscopedrenderingclient.md) | | | [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. | | [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. | | [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | @@ -118,7 +120,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [rendering](./kibana-plugin-core-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | @@ -216,6 +218,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-core-server.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-core-server.handlercontexttype.md). | | [Headers](./kibana-plugin-core-server.headers.md) | Http request headers to read. | +| [HttpResourcesRequestHandler](./kibana-plugin-core-server.httpresourcesrequesthandler.md) | Extended version of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) having access to [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) to respond with HTML or JS resources. | +| [HttpResourcesResponseOptions](./kibana-plugin-core-server.httpresourcesresponseoptions.md) | HTTP Resources response parameters | | [HttpResponsePayload](./kibana-plugin-core-server.httpresponsepayload.md) | Data send to the client as a response payload. | | [IBasePath](./kibana-plugin-core-server.ibasepath.md) | Access or manipulate the Kibana base path[BasePath](./kibana-plugin-core-server.basepath.md) | | [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-core-server.clusterclient.md). | @@ -245,6 +249,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. | | [RequestHandlerContextContainer](./kibana-plugin-core-server.requesthandlercontextcontainer.md) | An object that handles registration of http request context providers. | | [RequestHandlerContextProvider](./kibana-plugin-core-server.requesthandlercontextprovider.md) | Context provider for request handler. Extends request context object with provided functionality or data. | +| [RequestHandlerWrapper](./kibana-plugin-core-server.requesthandlerwrapper.md) | Type-safe wrapper for [RequestHandler](./kibana-plugin-core-server.requesthandler.md) function. | | [ResponseError](./kibana-plugin-core-server.responseerror.md) | Error message and optional data send to the client in case of error. | | [ResponseErrorAttributes](./kibana-plugin-core-server.responseerrorattributes.md) | Additional data to provide error details. | | [ResponseHeaders](./kibana-plugin-core-server.responseheaders.md) | Http response headers to set. | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md index 156f38fab0983..cecef7c923568 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md @@ -9,7 +9,7 @@ A function executed when route path matched requested resource path. Request han Signature: ```typescript -export declare type RequestHandler

= (context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) => IKibanaResponse | Promise>; +export declare type RequestHandler

= (context: RequestHandlerContext, request: KibanaRequest, response: ResponseFactory) => IKibanaResponse | Promise>; ``` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index 3c6bee114b6ab..0d640e52c3a03 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -8,7 +8,6 @@ ```typescript core: { - rendering: IScopedRenderingClient; savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index b65ae47f0e0c1..0966b91a4ebf2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [rendering](./kibana-plugin-core-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlerwrapper.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlerwrapper.md new file mode 100644 index 0000000000000..a9fe188ee2bff --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlerwrapper.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [RequestHandlerWrapper](./kibana-plugin-core-server.requesthandlerwrapper.md) + +## RequestHandlerWrapper type + +Type-safe wrapper for [RequestHandler](./kibana-plugin-core-server.requesthandler.md) function. + +Signature: + +```typescript +export declare type RequestHandlerWrapper = (handler: RequestHandler) => RequestHandler; +``` + +## Example + + +```typescript +export const wrapper: RequestHandlerWrapper = handler => { + return async (context, request, response) => { + // do some logic + ... + }; +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.responseheaders.md b/docs/development/core/server/kibana-plugin-core-server.responseheaders.md index 4551d1cab8632..fb7d6a10c6b6c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.responseheaders.md +++ b/docs/development/core/server/kibana-plugin-core-server.responseheaders.md @@ -9,9 +9,5 @@ Http response headers to set. Signature: ```typescript -export declare type ResponseHeaders = { - [header in KnownHeaders]?: string | string[]; -} & { - [header: string]: string | string[]; -}; +export declare type ResponseHeaders = Record | Record; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md index d8202545f0eae..a8894286de910 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md @@ -25,6 +25,5 @@ This is only internal for now, and will only be public when we expose the regist | [mappings](./kibana-plugin-core-server.savedobjectstype.mappings.md) | SavedObjectsTypeMappingDefinition | The [mapping definition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) for the type. | | [migrations](./kibana-plugin-core-server.savedobjectstype.migrations.md) | SavedObjectMigrationMap | An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. | | [name](./kibana-plugin-core-server.savedobjectstype.name.md) | string | The name of the type, which is also used as the internal id. | -| [namespaceAgnostic](./kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md) | boolean | Is the type global (true), or not (false). | | [namespaceType](./kibana-plugin-core-server.savedobjectstype.namespacetype.md) | SavedObjectsNamespaceType | The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) for the type. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md deleted file mode 100644 index e347421590482..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) > [namespaceAgnostic](./kibana-plugin-core-server.savedobjectstype.namespaceagnostic.md) - -## SavedObjectsType.namespaceAgnostic property - -> Warning: This API is now obsolete. -> -> Use `namespaceType` instead. -> - -Is the type global (true), or not (false). - -Signature: - -```typescript -namespaceAgnostic?: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md index 69912f9144980..3a3b0f7f3a9a5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.namespacetype.md @@ -9,5 +9,5 @@ The [namespace type](./kibana-plugin-core-server.savedobjectsnamespacetype.md) f Signature: ```typescript -namespaceType?: SavedObjectsNamespaceType; +namespaceType: SavedObjectsNamespaceType; ``` diff --git a/docs/images/add_remote_cluster.png b/docs/images/add_remote_cluster.png index 376b1d8392366..160d29b741c62 100755 Binary files a/docs/images/add_remote_cluster.png and b/docs/images/add_remote_cluster.png differ diff --git a/docs/images/auto_follow_pattern.png b/docs/images/auto_follow_pattern.png index 3bf86458eddd7..f80de9352280f 100755 Binary files a/docs/images/auto_follow_pattern.png and b/docs/images/auto_follow_pattern.png differ diff --git a/docs/images/cross-cluster-replication-list-view.png b/docs/images/cross-cluster-replication-list-view.png new file mode 100755 index 0000000000000..4c45174cff7f1 Binary files /dev/null and b/docs/images/cross-cluster-replication-list-view.png differ diff --git a/docs/images/follower_indices.png b/docs/images/follower_indices.png old mode 100644 new mode 100755 index f103bb3cf2acf..505adeb45ae23 Binary files a/docs/images/follower_indices.png and b/docs/images/follower_indices.png differ diff --git a/docs/images/remote-clusters-list-view.png b/docs/images/remote-clusters-list-view.png new file mode 100755 index 0000000000000..c28379863b74b Binary files /dev/null and b/docs/images/remote-clusters-list-view.png differ diff --git a/docs/ingest_manager/index-templates.asciidoc b/docs/ingest_manager/index-templates.asciidoc new file mode 100644 index 0000000000000..e19af63c3116f --- /dev/null +++ b/docs/ingest_manager/index-templates.asciidoc @@ -0,0 +1,7 @@ +# Elasticsearch Index Templates + +## Generation + +* Index templates are generated from `YAML` files contained in the package. +* There is one index template per dataset. +* For the generation of an index template, all `yml` files contained in the package subdirectory `dataset/DATASET_NAME/fields/` are used. diff --git a/docs/ingest_manager/index.asciidoc b/docs/ingest_manager/index.asciidoc index 22afa88c919e4..866935d1fa580 100644 --- a/docs/ingest_manager/index.asciidoc +++ b/docs/ingest_manager/index.asciidoc @@ -199,3 +199,10 @@ The new ingest pipeline is expected to still work with the data coming from olde In case of a breaking change in the data structure, the new ingest pipeline is also expected to deal with this change. In case there are breaking changes which cannot be dealt with in an ingest pipeline, a new package has to be created. Each package lists its minimal required agent version. In case there are agents enrolled with an older version, the user is notified to upgrade these agents as otherwise the new configs cannot be rolled out. + +=== Generated assets + +When a package is installed or upgraded, certain Kibana and Elasticsearch assets are generated from . These follow the naming conventions explained above (see "indexing strategy") and contain configuration for the elastic stack that makes ingesting and displaying data work with as little user interaction as possible. + +* link:index-templates.asciidoc[Elasticsearch Index Templates] +* Kibana Index Patterns diff --git a/docs/management/managing-ccr.asciidoc b/docs/management/managing-ccr.asciidoc new file mode 100644 index 0000000000000..b2db5a80cfe7e --- /dev/null +++ b/docs/management/managing-ccr.asciidoc @@ -0,0 +1,73 @@ +[role="xpack"] +[[managing-cross-cluster-replication]] +== Cross-Cluster Replication + +Use *Cross-Cluster Replication* to reproduce indices in +remote clusters on a local cluster. {ref}/xpack-ccr.html[Cross-cluster replication] +is commonly used to provide remote backups for disaster recovery and for +geo-proximite copies of data. + +To get started, go to *Management > Cross-Cluster Replication*. + +[role="screenshot"] +image::images/cross-cluster-replication-list-view.png[][Cross-cluster replication list view] + +[float] +=== Prerequisites + +* You must have a {ref}/modules-remote-clusters.html[remote cluster]. +* Leader indices must meet {ref}/ccr-requirements.html[these requirements]. +* The Elasticsearch version of the local cluster must be the same as or newer than the remote cluster. +Refer to {ref}/ccr-overview.html[this document] for more information. + +[float] +[[configure-replication]] +=== Configure replication + +Replication requires a leader index, the index being replicated, and a +follower index, which will contain the leader index's replicated data. +The follower index is passive in that it can read requests and searches, +but cannot accept direct writes. Only the leader index is active for direct writes. + +You can configure follower indices in two ways: + +* Create specific follower indices +* Create follower indices from an auto-follow pattern + +[float] +==== Create specific follower indices + +To replicate data from existing indices, or set up local followers on a case-by-case basis, +go to *Follower indices*. When you create the follower index, you must reference the +remote cluster and the leader index that you created in the remote cluster. + +[role="screenshot"] +image::images/follower_indices.png[][UI for adding follower indices] + +[float] +==== Create follower indices from an auto-follow pattern + +To automatically detect and follow new indices when they are created on a remote cluster, +go to *Auto-follow patterns*. Creating an auto-follow pattern is useful when you have +time series data, like event logs, on the remote cluster that is created or rolled over on a daily basis. + +When creating the pattern, you must reference the remote cluster that you +connected to your local cluster. You must also specify a collection of index patterns +that match the indices you want to automatically follow. + +Once you configure an +auto-follow pattern, any time a new index with a name that matches the pattern is +created in the remote cluster, a follower index is automatically configured in the local cluster. + +[role="screenshot"] +image::images/auto_follow_pattern.png[UI for adding an auto-follow pattern] + +[float] +[[manage-replication]] +=== Manage replication + +Use the list views in *Cross-Cluster Replication* to monitor whether the replication is active and +pause and resume replication. You can also edit and remove the follower indices and auto-follow patterns. + +For an example of cross-cluster replication, +refer to https://www.elastic.co/blog/bi-directional-replication-with-elasticsearch-cross-cluster-replication-ccr[Bi-directional replication with Elasticsearch cross-cluster replication]. diff --git a/docs/management/managing-remote-clusters.asciidoc b/docs/management/managing-remote-clusters.asciidoc index 6b69cfef5b768..00ec5c7d2ddea 100644 --- a/docs/management/managing-remote-clusters.asciidoc +++ b/docs/management/managing-remote-clusters.asciidoc @@ -1,67 +1,39 @@ [[working-remote-clusters]] == Remote Clusters -{kib} *Management* provides user interfaces for working with data from remote -clusters and managing the {ccr} process. You can replicate indices from a -leader remote cluster to a follower index in a local cluster. The local follower indices -can be used to provide remote backups for disaster recovery or for geo-proximite copies of data. +Use *Remote Clusters* to establish a unidirectional +connection from your cluster to other clusters. This functionality is +required for {ref}/xpack-ccr.html[cross-cluster replication] and +{ref}/modules-cross-cluster-search.html[cross-cluster search]. -Before using these features, you should be familiar with the following concepts: +To get started, go to *Management > Remote Clusters*. -* {ref}/xpack-ccr.html[{ccr-cap}] -* {ref}/modules-cross-cluster-search.html[{ccs-cap}] -* {ref}/cross-cluster-configuring.html[Cross-cluster security requirements] +[role="screenshot"] +image::images/remote-clusters-list-view.png[Remote Clusters list view, including Add a remote cluster button] [float] [[managing-remote-clusters]] -== Managing remote clusters - -*Remote clusters* helps you manage remote clusters for use with -{ccs} and {ccr}. You can add and remove remote clusters and check their connectivity. +=== Add a remote cluster -Before you use this feature, you should be familiar with the concept of -{ref}/modules-remote-clusters.html[remote clusters]. +A {ref}/modules-remote-clusters.html[remote cluster] connection works by configuring a remote cluster and +connecting to a limited number of nodes, called {ref}/modules-remote-clusters.html#sniff-mode[seed nodes], +in that cluster. +Alternatively, you can define a single proxy address for the remote cluster. -Go to *Management > Elasticsearch > Remote clusters* to create or manage your remotes. +By default, a cross-cluster request, such as a cross-cluster search or +replication request, fails if any cluster in the request is unavailable. +To skip a cluster when its unavailable, +set *Skip if unavailable* to true. -To set up a new remote, click *Add a remote cluster*. Give the cluster a unique name -and define the seed nodes for cluster discovery. You can edit or remove your remote clusters -from the *Remote clusters* list view. +Once you add a remote cluster, you can configure <> +to reproduce indices in the remote cluster on a local cluster. [role="screenshot"] image::images/add_remote_cluster.png[][UI for adding a remote cluster] -Once a remote cluster is registered, you can use the tools under *{ccr-cap}* -to add and manage follower indices on the local cluster, and replicate data from -indices on the remote cluster based on an auto-follow index pattern. - [float] -[[managing-cross-cluster-replication]] -== [xpack]#Managing {ccr}# - -*{ccr-cap}* helps you create and manage the {ccr} process. -If you want to replicate data from existing indices, or set up -local followers on a case-by-case basis, go to *Follower indices*. -If you want to automatically detect and follow new indices when they are created -on a remote cluster, you can do so from *Auto-follow patterns*. - -Creating an auto-follow pattern is useful when you have time-series data, like a logs index, on the -remote cluster that is created or rolled over on a daily basis. Once you have configured an -auto-follow pattern, any time a new index with a name that matches the pattern is -created in the remote cluster, a follower index is automatically configured in the local cluster. - -From the same view, you can also see a list of your saved auto-follow patterns for -a given remote cluster, and monitor whether the replication is active. +[[manage-remote-clusters]] +=== Manage remote clusters -Before you use these features, you should be familiar with the following concepts: - -* {ref}/ccr-requirements.html[Requirements for leader indices] -* {ref}/ccr-auto-follow.html[Automatically following indices] - -To get started, go to *Management > Elasticsearch > {ccr-cap}*. - -[role="screenshot"] -image::images/auto_follow_pattern.png[][UI for adding an auto-follow pattern] - -[role="screenshot"] -image::images/follower_indices.png[][UI for adding follower indices] +From the *Remote Clusters* list view, you can drill down into each cluster and +view its status. You can also edit and delete a cluster. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index fd835bde83322..a5503969a3ec1 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -23,7 +23,7 @@ For more {kib} configuration settings, see <>. [role="exclude",id="uptime-security"] == Uptime security -This page has moved. Please see the new section in the {uptime-guide}/uptime-security.html[Uptime Monitoring Guide]. +This page has moved. Please see the new section in the {heartbeat-ref}/securing-heartbeat.html[Uptime Monitoring Guide]. [role="exclude",id="infra-read-only-access"] == Configure source read-only access diff --git a/docs/uptime-guide/index.asciidoc b/docs/uptime-guide/index.asciidoc index 7bbc01bb303f1..09763182fa88f 100644 --- a/docs/uptime-guide/index.asciidoc +++ b/docs/uptime-guide/index.asciidoc @@ -12,4 +12,3 @@ include::install.asciidoc[] include::deployment-arch.asciidoc[] -include::security.asciidoc[] diff --git a/docs/uptime-guide/install.asciidoc b/docs/uptime-guide/install.asciidoc index e7c50bb7604ce..0ed1270ca92ce 100644 --- a/docs/uptime-guide/install.asciidoc +++ b/docs/uptime-guide/install.asciidoc @@ -56,6 +56,11 @@ Additional information is available in {heartbeat-ref}/heartbeat-configuration.h [role="screenshot"] image::images/uptime-setup.png[Installation instructions on the Uptime page in Kibana] +[[setup-security]] +=== Step 4: Setup Security + +Secure your installation by following the {heartbeat-ref}/securing-heartbeat.html[Secure Heartbeat] documentation. + [float] ==== Important considerations diff --git a/docs/uptime-guide/security.asciidoc b/docs/uptime-guide/security.asciidoc deleted file mode 100644 index 0c6fa4c6c4f56..0000000000000 --- a/docs/uptime-guide/security.asciidoc +++ /dev/null @@ -1,60 +0,0 @@ -[[uptime-security]] -== Elasticsearch Security - -If you use Elasticsearch security, you'll need to enable certain privileges for users -that would like to access the Uptime app. For example, create user and support roles to implement the privileges: - -[float] -=== Create a role - -You'll need a role that lets you access the Heartbeat indices, which by default are `heartbeat-*`. -You can create this with the following request: - -["source","sh",subs="attributes,callouts"] ---------------------------------------------------------------- -PUT /_security/role/uptime -{ "indices" : [ - { - "names" : [ - "heartbeat-*" - ], - "privileges" : [ - "read", - "view_index_metadata" - ], - "field_security" : { - "grant" : [ - "*" - ] - }, - "allow_restricted_indices" : false - } - ], - "transient_metadata" : { - "enabled" : true - } -} ---------------------------------------------------------------- -// CONSOLE - -[float] -=== Assign the role to a user - -Next, you'll need to create a user with both the `uptime` role, and another role with sufficient {kibana-ref}/kibana-privileges.html[Kibana privileges], -such as the `kibana_admin` role. -You can do this with the following request: - -["source","sh",subs="attributes,callouts"] ---------------------------------------------------------------- -PUT /_security/user/jacknich -{ - "password" : "j@rV1s", - "roles" : [ "uptime", "kibana_admin" ], - "full_name" : "Jack Nicholson", - "email" : "jacknich@example.com", - "metadata" : { - "intelligence" : 7 - } -} ---------------------------------------------------------------- -// CONSOLE diff --git a/docs/uptime/images/alert-flyout.png b/docs/uptime/images/alert-flyout.png new file mode 100644 index 0000000000000..7fc1e3d9aefe2 Binary files /dev/null and b/docs/uptime/images/alert-flyout.png differ diff --git a/docs/uptime/images/check-history.png b/docs/uptime/images/check-history.png index 6418495eee9ed..91565bf59aa7f 100644 Binary files a/docs/uptime/images/check-history.png and b/docs/uptime/images/check-history.png differ diff --git a/docs/uptime/images/error-list.png b/docs/uptime/images/error-list.png deleted file mode 100644 index 99f017f2945a5..0000000000000 Binary files a/docs/uptime/images/error-list.png and /dev/null differ diff --git a/docs/uptime/images/monitor-charts.png b/docs/uptime/images/monitor-charts.png index dbfa43f47656e..522f34662657e 100644 Binary files a/docs/uptime/images/monitor-charts.png and b/docs/uptime/images/monitor-charts.png differ diff --git a/docs/uptime/images/observability_integrations.png b/docs/uptime/images/observability_integrations.png index d5c612c7589ca..6589c0c5565dd 100644 Binary files a/docs/uptime/images/observability_integrations.png and b/docs/uptime/images/observability_integrations.png differ diff --git a/docs/uptime/images/settings.png b/docs/uptime/images/settings.png new file mode 100644 index 0000000000000..dd36f0a6d702b Binary files /dev/null and b/docs/uptime/images/settings.png differ diff --git a/docs/uptime/images/snapshot-view.png b/docs/uptime/images/snapshot-view.png index 020396d0f3e4c..1fce2e9592c14 100644 Binary files a/docs/uptime/images/snapshot-view.png and b/docs/uptime/images/snapshot-view.png differ diff --git a/docs/uptime/images/status-bar.png b/docs/uptime/images/status-bar.png index e0e9b27555900..8d242789cdccd 100644 Binary files a/docs/uptime/images/status-bar.png and b/docs/uptime/images/status-bar.png differ diff --git a/docs/uptime/index.asciidoc b/docs/uptime/index.asciidoc index 785b9f818f5bf..a355f8ecf4843 100644 --- a/docs/uptime/index.asciidoc +++ b/docs/uptime/index.asciidoc @@ -12,8 +12,10 @@ To get started with Elastic Uptime, refer to {uptime-guide}/install-uptime.html[ * <> * <> +* <> -- include::overview.asciidoc[] include::monitor.asciidoc[] +include::settings.asciidoc[] diff --git a/docs/uptime/monitor.asciidoc b/docs/uptime/monitor.asciidoc index d54fd02c7c069..8a4be1f11a721 100644 --- a/docs/uptime/monitor.asciidoc +++ b/docs/uptime/monitor.asciidoc @@ -5,21 +5,24 @@ The Monitor page will help you get further insight into the performance of a specific network endpoint. You'll see a detailed visualization of the monitor's request duration over time, as well as the `up`/`down` -status over time. +status over time. You can also also detect anomalies in response time data +by configuring Machine Learning jobs on this page. [float] -=== Status bar +=== Status panel [role="screenshot"] image::uptime/images/status-bar.png[Status bar] -The Status bar displays a quick summary of the latest information +The Status panel displays a quick summary of the latest information regarding your monitor. You can view its latest status, click a link to visit the targeted URL, see its most recent request duration, and determine the amount of time that has elapsed since the last check. -You can use the Status bar to get a quick summary of current performance, -beyond simply knowing if the monitor is `up` or `down`. +When two Heartbeat instances are configured in different geographic locations +the map will show each location as a pinpoint on the map, along with the +amount of time elapsed since data was last received from that location. + [float] === Monitor charts @@ -32,12 +35,14 @@ date range. These charts can help you gain insight into how quickly requests are by the targeted endpoint, and give you a sense of how frequently a host or endpoint was down in your selected timespan. -The first chart displays request duration information for your monitor. +The Monitor duration chart displays request duration information for your monitor. The area surrounding the line is the range of request time for the corresponding -bucket. The line is the average time. +bucket. The line is the average time. Anomaly detection using Machine Learning +can be configured in the upper right hand of this panel. When response times change +in an unexpected way the time range in which they occurred will be given filled with a color. -Next, is a graphical representation of the check statuses over time. Hover over -the charts to display crosshairs with more specific numeric data. +The pings over time chart is a graphical representation of the check statuses over time. +Hover over the charts to display crosshairs with more specific numeric data. [role="screenshot"] image::uptime/images/crosshair-example.png[Chart crosshair] @@ -49,6 +54,6 @@ image::uptime/images/crosshair-example.png[Chart crosshair] image::uptime/images/check-history.png[Check history view] The Check history displays the total count of this monitor's checks for the selected -date range. You can additionally filter the checks by `status` to help find recent problems +date range. You can additionally filter the checks by status and location to help find recent problems on a per-check basis. This table can help you gain some insight into more granular details about recent individual data points Heartbeat is logging about your host or endpoint. diff --git a/docs/uptime/overview.asciidoc b/docs/uptime/overview.asciidoc index 098ce12a56991..71c09c968e512 100644 --- a/docs/uptime/overview.asciidoc +++ b/docs/uptime/overview.asciidoc @@ -21,12 +21,12 @@ This control allows you to use automated filter options, as well as input custom text to select specific monitors by field, URL, ID, and other attributes. [float] -=== Snapshot view +=== Snapshot panel [role="screenshot"] image::uptime/images/snapshot-view.png[Snapshot view] -This view is intended to quickly give you a sense of the overall +This panel is intended to quickly give you a sense of the overall status of the environment you're monitoring, or a subset of those monitors. Here, you can see the total number of detected monitors within the selected Uptime date range. In addition to the total, the counts for the number of monitors @@ -49,6 +49,17 @@ way to navigate to a more in-depth visualization for interesting hosts or endpoi This table includes information like the most recent status, when the monitor was last checked, its ID and URL, its IP address, and a dedicated sparkline showing its check status over time. +[float] +=== Creating and managing alerts + +[role="screenshot"] +image::uptime/images/alert-flyout.png[Create alert flyout] + +To receive alerts when a monitor goes down, use the alerting menu at the top of the +overview page. Use a query in the alert flyout to determine which monitors to check +with your alert. If you already have a query in the overview page search bar it will +be carried over into this box. + [float] === Observability integrations @@ -60,14 +71,3 @@ Docker related host information, it will provide links to open the Metrics app o for this host. Additionally, this feature supplies links to simply filter the other views on the host's IP address, to help you quickly determine if these other solutions contain data relevant to your current interest. - -[float] -=== Error list - -[role="screenshot"] -image::uptime/images/error-list.png[Error list] - -The Error list displays aggregations of errors that Heartbeat has logged. Errors are -displayed by Error type, monitor ID, and message. Clicking a monitor's ID will take you -to the corresponding Monitor view, which can provide you richer information about the individual -data points that are resulting in the displayed errors. diff --git a/docs/uptime/settings.asciidoc b/docs/uptime/settings.asciidoc new file mode 100644 index 0000000000000..55da6e802bec6 --- /dev/null +++ b/docs/uptime/settings.asciidoc @@ -0,0 +1,27 @@ +[role="xpack"] +[[uptime-settings]] + +== Settings + +[role="screenshot"] +image::uptime/images/settings.png[Filter bar] + +The Uptime settings page lets you change which Heartbeat indices are displayed +by the uptime app. Users must have the 'all' permission to modify items on this page. +Uptime settings apply to the current space only. Use different settings in different +spaces to segment different uptime use cases and domains. + +As an example, imagine your organization has one team for internal IT services, and another +for public services. Each team operates independently and is only responsible for its +own services. In this scenario, you might set up separate Heartbeat instances for each team, +writing out to index patterns named `it-heartbeat-\*`, and `external-heartbeat-\*`. You would +create separate roles and users for each in Elasticsearch, each with access to their own spaces, +named `it` and `external` respectively. Within each space you would navigate to the settings page +and set the correct index pattern to match only the indices that space is allowed to access. + +Note that the pattern set here only restricts what the Uptime app shows. Users may still be able +to manually query Elasticsearch for data outside this pattern! + +See the <> +and {heartbeat-ref}/securing-heartbeat.html[Heartbeat security] +docs for more information. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index fa34802abe2a9..a4ba320e826b1 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -13,7 +13,7 @@ indices, clusters, licenses, UI settings, index patterns, spaces, and more. [cols="50, 50"] |=== -a| <> +a| <> Replicate indices on a remote cluster and copy them to a follower index on a local cluster. This is important for @@ -85,7 +85,8 @@ set the timespan for notification messages, and much more. | <> -Centrally manage your alerts from across {kib}. Create and manage re-usable connectors for triggering actions. +Centrally manage your alerts across {kib}. Create and manage reusable +connectors for triggering actions. | <> @@ -125,6 +126,8 @@ include::{kib-repo-dir}/management/alerting/connector-management.asciidoc[] include::{kib-repo-dir}/management/managing-beats.asciidoc[] +include::{kib-repo-dir}/management/managing-ccr.asciidoc[] + include::{kib-repo-dir}/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc[] include::{kib-repo-dir}/management/index-lifecycle-policies/create-policy.asciidoc[] diff --git a/examples/embeddable_examples/server/todo_saved_object.ts b/examples/embeddable_examples/server/todo_saved_object.ts index 0f67c53cfa3e1..58da2014de498 100644 --- a/examples/embeddable_examples/server/todo_saved_object.ts +++ b/examples/embeddable_examples/server/todo_saved_object.ts @@ -22,7 +22,7 @@ import { SavedObjectsType } from 'kibana/server'; export const todoSavedObject: SavedObjectsType = { name: 'todo', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { title: { diff --git a/package.json b/package.json index c60cf5234c9f7..21e9f67e6206a 100644 --- a/package.json +++ b/package.json @@ -119,8 +119,8 @@ "dependencies": { "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", - "@elastic/apm-rum": "^4.6.0", - "@elastic/charts": "18.2.2", + "@elastic/apm-rum": "^5.1.1", + "@elastic/charts": "18.3.0", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", "@elastic/eui": "21.0.1", @@ -377,7 +377,7 @@ "@types/recompose": "^0.30.6", "@types/redux-actions": "^2.6.1", "@types/request": "^2.48.2", - "@types/selenium-webdriver": "^4.0.5", + "@types/selenium-webdriver": "^4.0.9", "@types/semver": "^5.5.0", "@types/sinon": "^7.0.13", "@types/strip-ansi": "^3.0.0", @@ -463,6 +463,7 @@ "load-grunt-config": "^3.0.1", "mocha": "^7.1.1", "mock-http-server": "1.3.0", + "ms-chromium-edge-driver": "^0.2.3", "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", @@ -481,7 +482,7 @@ "react-textarea-autosize": "^7.1.2", "regenerate": "^1.4.0", "sass-lint": "^1.12.1", - "selenium-webdriver": "^4.0.0-alpha.5", + "selenium-webdriver": "^4.0.0-alpha.7", "simple-git": "1.116.0", "simplebar-react": "^2.1.0", "sinon": "^7.4.2", diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 66f17ab579ec3..f4b91d154cbb8 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -136,7 +136,7 @@ export const schema = Joi.object() browser: Joi.object() .keys({ type: Joi.string() - .valid('chrome', 'firefox', 'ie') + .valid('chrome', 'firefox', 'ie', 'msedge') .default('chrome'), logPollingMs: Joi.number().default(100), diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 7c5d6a62a11ca..c8614b1df9d5d 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/charts": "18.2.2", + "@elastic/charts": "18.3.0", "@elastic/eui": "21.0.1", "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 368d1f47e9c3f..80f12dd78214d 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1252,26 +1252,27 @@ import { npStart: { plugins } } from 'ui/new_platform'; In server code, `core` can be accessed from either `server.newPlatform` or `kbnServer.newPlatform`. There are not currently very many services available on the server-side: -| Legacy Platform | New Platform | Notes | -| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | -| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | -| `server.renderApp()` / `server.renderAppWithDefaultConfig()` | [`context.rendering.render()`](/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.render.md) | [Examples](./MIGRATION_EXAMPLES.md#render-html-content) | -| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.basepath.md) | | +| Legacy Platform | New Platform | Notes | +| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | +| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | +| `server.renderApp()` | [`response.renderCoreApp()`](docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md) | [Examples](./MIGRATION_EXAMPLES.md#render-html-content) | +| `server.renderAppWithDefaultConfig()` | [`response.renderAnonymousCoreApp()`](docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md) | [Examples](./MIGRATION_EXAMPLES.md#render-html-content) | +| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.basepath.md) | | | `server.plugins.elasticsearch.getCluster('data')` | [`context.core.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) | | | `server.plugins.elasticsearch.getCluster('admin')` | [`context.core.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) | | -| `server.plugins.elasticsearch.createCluster(...)` | [`core.elasticsearch.legacy.createClient`](/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | | -| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactoryProvider`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | | -| `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | | +| `server.plugins.elasticsearch.createCluster(...)` | [`core.elasticsearch.legacy.createClient`](/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | | +| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactoryProvider`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | | +| `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | | | `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createscopedrepository.md) | | -| `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.getscopedclient.md) | | -| `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md) | | +| `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.getscopedclient.md) | | +| `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md) | | | `request.getUiSettingsService` | [`context.core.uiSettings.client`](/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md) | | -| `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration | -| `kibana.Plugin.savedObjectSchemas` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | -| `kibana.Plugin.mappings` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | -| `kibana.Plugin.migrations` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | -| `kibana.Plugin.savedObjectsManagement` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | +| `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration | +| `kibana.Plugin.savedObjectSchemas` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | +| `kibana.Plugin.mappings` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | +| `kibana.Plugin.migrations` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | +| `kibana.Plugin.savedObjectsManagement` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) | _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-core-server.coresetup.md)_ @@ -1494,8 +1495,9 @@ The above example looks in the new platform as ``` The [request handler context](/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md) exposed the next scoped **core** services: -| Legacy Platform | New Platform | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------| + +| Legacy Platform | New Platform | +| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `request.getSavedObjectsClient` | [`context.savedObjects.client`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md) | | `server.plugins.elasticsearch.getCluster('admin')` | [`context.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) | | `server.plugins.elasticsearch.getCluster('data')` | [`context.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) | diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 37d0b9297ed3c..8c5fe4875aaea 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -700,21 +700,15 @@ application.register({ ## Render HTML Content You can return a blank HTML page bootstrapped with the core application bundle from an HTTP route handler -via the `rendering` context. You may wish to do this if you are rendering a chromeless application with a +via the `httpResources` service. You may wish to do this if you are rendering a chromeless application with a custom application route or have other custom rendering needs. -```ts -router.get( +```typescript +httpResources.register( { path: '/chromeless', validate: false }, (context, request, response) => { - const { http, rendering } = context.core; - - return response.ok({ - body: await rendering.render(), // generates an HTML document - headers: { - 'content-security-policy': http.csp.header, - }, - }); + //... some logic + return response.renderCoreApp(); } ); ``` @@ -724,18 +718,12 @@ comprises all UI Settings that are *user provided*, then injected into the page. You may wish to exclude fetching this data if not authorized or to slim the page size. -```ts -router.get( - { path: '/', validate: false }, +```typescript +httpResources.register( + { path: '/', validate: false, options: { authRequired: false } }, (context, request, response) => { - const { http, rendering } = context.core; - - return response.ok({ - body: await rendering.render({ includeUserSettings: false }), - headers: { - 'content-security-policy': http.csp.header, - }, - }); + //... some logic + return response.renderAnonymousCoreApp(); } ); ``` diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index a75eb04fa0120..ca9dfde2e71dc 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -38,6 +38,7 @@ export { LifecycleResponseFactory, RedirectResponseOptions, RequestHandler, + RequestHandlerWrapper, ResponseError, ResponseErrorAttributes, ResponseHeaders, diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts index 50d3d7b47bf8d..050881472bc80 100644 --- a/src/core/server/http/lifecycle/on_pre_response.ts +++ b/src/core/server/http/lifecycle/on_pre_response.ts @@ -148,7 +148,7 @@ function findHeadersIntersection( log: Logger ) { Object.keys(headers).forEach(headerName => { - if (responseHeaders[headerName] !== undefined) { + if (Reflect.has(responseHeaders, headerName)) { log.warn(`onPreResponseHandler rewrote a response header [${headerName}].`); } }); diff --git a/src/core/server/http/router/error_wrapper.ts b/src/core/server/http/router/error_wrapper.ts index 8f895753c38c3..af99812eff4b3 100644 --- a/src/core/server/http/router/error_wrapper.ts +++ b/src/core/server/http/router/error_wrapper.ts @@ -18,20 +18,10 @@ */ import Boom from 'boom'; -import { KibanaRequest } from './request'; -import { KibanaResponseFactory } from './response'; -import { RequestHandler } from './router'; -import { RequestHandlerContext } from '../../../server'; -import { RouteMethod } from './route'; +import { RequestHandlerWrapper } from './router'; -export const wrapErrors = ( - handler: RequestHandler -): RequestHandler => { - return async ( - context: RequestHandlerContext, - request: KibanaRequest, - response: KibanaResponseFactory - ) => { +export const wrapErrors: RequestHandlerWrapper = handler => { + return async (context, request, response) => { try { return await handler(context, request, response); } catch (e) { diff --git a/src/core/server/http/router/headers.ts b/src/core/server/http/router/headers.ts index 19eaee5081996..b79cc0d325f1e 100644 --- a/src/core/server/http/router/headers.ts +++ b/src/core/server/http/router/headers.ts @@ -56,9 +56,9 @@ export type Headers = { [header in KnownHeaders]?: string | string[] | undefined * Http response headers to set. * @public */ -export type ResponseHeaders = { [header in KnownHeaders]?: string | string[] } & { - [header: string]: string | string[]; -}; +export type ResponseHeaders = + | Record + | Record; const normalizeHeaderField = (field: string) => field.trim().toLowerCase(); diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index d254f391ca5e4..83ceff4a25d86 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -18,7 +18,7 @@ */ export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers'; -export { Router, RequestHandler, IRouter, RouteRegistrar } from './router'; +export { Router, RequestHandler, RequestHandlerWrapper, IRouter, RouteRegistrar } from './router'; export { KibanaRequest, KibanaRequestEvents, diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index bb56ee3727d1a..b4e7fc2a989b6 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -98,7 +98,7 @@ export interface IRouter { * Wrap a router handler to catch and converts legacy boom errors to proper custom errors. * @param handler {@link RequestHandler} - a route handler to wrap */ - handleLegacyErrors: (handler: RequestHandler) => RequestHandler; + handleLegacyErrors: RequestHandlerWrapper; /** * Returns all routes registered with this router. @@ -237,9 +237,7 @@ export class Router implements IRouter { return [...this.routes]; } - public handleLegacyErrors(handler: RequestHandler): RequestHandler { - return wrapErrors(handler); - } + public handleLegacyErrors = wrapErrors; private async handle({ routeSchemas, @@ -316,9 +314,33 @@ export type RequestHandler< P = unknown, Q = unknown, B = unknown, - Method extends RouteMethod = any + Method extends RouteMethod = any, + ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory > = ( context: RequestHandlerContext, request: KibanaRequest, - response: KibanaResponseFactory + response: ResponseFactory ) => IKibanaResponse | Promise>; + +/** + * Type-safe wrapper for {@link RequestHandler} function. + * @example + * ```typescript + * export const wrapper: RequestHandlerWrapper = handler => { + * return async (context, request, response) => { + * // do some logic + * ... + * }; + * } + * ``` + * @public + */ +export type RequestHandlerWrapper = < + P, + Q, + B, + Method extends RouteMethod = any, + ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory +>( + handler: RequestHandler +) => RequestHandler; diff --git a/src/core/server/http_resources/http_resources_service.mock.ts b/src/core/server/http_resources/http_resources_service.mock.ts new file mode 100644 index 0000000000000..4536b0898cad9 --- /dev/null +++ b/src/core/server/http_resources/http_resources_service.mock.ts @@ -0,0 +1,50 @@ +/* + * 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 { httpServerMock } from '../http/http_server.mocks'; +import { HttpResources, HttpResourcesServiceToolkit } from './types'; + +const createHttpResourcesMock = (): jest.Mocked => ({ + register: jest.fn(), +}); + +function createInternalHttpResourcesSetup() { + return { + createRegistrar: createHttpResourcesMock, + }; +} + +function createHttpResourcesResponseFactory() { + const mocked: jest.Mocked = { + renderCoreApp: jest.fn(), + renderAnonymousCoreApp: jest.fn(), + renderHtml: jest.fn(), + renderJs: jest.fn(), + }; + + return { + ...httpServerMock.createResponseFactory(), + ...mocked, + }; +} + +export const httpResourcesMock = { + createRegistrar: createHttpResourcesMock, + createSetupContract: createInternalHttpResourcesSetup, + createResponseFactory: createHttpResourcesResponseFactory, +}; diff --git a/src/core/server/http_resources/http_resources_service.test.ts b/src/core/server/http_resources/http_resources_service.test.ts new file mode 100644 index 0000000000000..e6f129ba12d78 --- /dev/null +++ b/src/core/server/http_resources/http_resources_service.test.ts @@ -0,0 +1,258 @@ +/* + * 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 { IRouter, RouteConfig } from '../http'; + +import { coreMock } from '../mocks'; +import { mockCoreContext } from '../core_context.mock'; +import { httpServiceMock } from '../http/http_service.mock'; +import { httpServerMock } from '../http/http_server.mocks'; +import { renderingMock } from '../rendering/rendering_service.mock'; +import { HttpResourcesService, SetupDeps } from './http_resources_service'; +import { httpResourcesMock } from './http_resources_service.mock'; + +const coreContext = mockCoreContext.create(); + +describe('HttpResources service', () => { + let service: HttpResourcesService; + let setupDeps: SetupDeps; + let router: jest.Mocked; + const kibanaRequest = httpServerMock.createKibanaRequest(); + const context = { core: coreMock.createRequestHandlerContext() }; + describe('#createRegistrar', () => { + beforeEach(() => { + setupDeps = { + http: httpServiceMock.createSetupContract(), + rendering: renderingMock.createSetupContract(), + }; + service = new HttpResourcesService(coreContext); + router = httpServiceMock.createRouter(); + }); + + describe('register', () => { + describe('renderCoreApp', () => { + it('formats successful response', async () => { + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderCoreApp(); + }); + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(setupDeps.rendering.render).toHaveBeenCalledWith( + kibanaRequest, + context.core.uiSettings.client, + { + includeUserSettings: true, + } + ); + }); + + it('can attach headers, except the CSP header', async () => { + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderCoreApp({ + headers: { + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); + + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: '', + headers: { + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + }); + describe('renderAnonymousCoreApp', () => { + it('formats successful response', async () => { + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderAnonymousCoreApp(); + }); + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(setupDeps.rendering.render).toHaveBeenCalledWith( + kibanaRequest, + context.core.uiSettings.client, + { + includeUserSettings: false, + } + ); + }); + + it('can attach headers, except the CSP header', async () => { + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderAnonymousCoreApp({ + headers: { + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); + + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: '', + headers: { + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + }); + describe('renderHtml', () => { + it('formats successful response', async () => { + const htmlBody = ''; + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderHtml({ body: htmlBody }); + }); + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: htmlBody, + headers: { + 'content-type': 'text/html', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + + it('can attach headers, except the CSP & "content-type" headers', async () => { + const htmlBody = ''; + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderHtml({ + body: htmlBody, + headers: { + 'content-type': 'text/html5', + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); + + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: htmlBody, + headers: { + 'content-type': 'text/html', + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + }); + describe('renderJs', () => { + it('formats successful response', async () => { + const jsBody = 'alert(1);'; + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderJs({ body: jsBody }); + }); + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: jsBody, + headers: { + 'content-type': 'text/javascript', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + + it('can attach headers, except the CSP & "content-type" headers', async () => { + const jsBody = 'alert(1);'; + const routeConfig: RouteConfig = { path: '/', validate: false }; + const { createRegistrar } = await service.setup(setupDeps); + const { register } = createRegistrar(router); + register(routeConfig, async (ctx, req, res) => { + return res.renderJs({ + body: jsBody, + headers: { + 'content-type': 'text/html', + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }); + }); + + const [[, routeHandler]] = router.get.mock.calls; + + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler(context, kibanaRequest, responseFactory); + + expect(responseFactory.ok).toHaveBeenCalledWith({ + body: jsBody, + headers: { + 'content-type': 'text/javascript', + 'x-kibana': '42', + 'content-security-policy': + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", + }, + }); + }); + }); + }); + }); +}); diff --git a/src/core/server/http_resources/http_resources_service.ts b/src/core/server/http_resources/http_resources_service.ts new file mode 100644 index 0000000000000..bc79ad68f4099 --- /dev/null +++ b/src/core/server/http_resources/http_resources_service.ts @@ -0,0 +1,130 @@ +/* + * 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 { RequestHandlerContext } from 'src/core/server'; + +import { CoreContext } from '../core_context'; +import { + IRouter, + RouteConfig, + InternalHttpServiceSetup, + KibanaRequest, + KibanaResponseFactory, +} from '../http'; + +import { Logger } from '../logging'; +import { InternalRenderingServiceSetup } from '../rendering'; +import { CoreService } from '../../types'; + +import { + InternalHttpResourcesSetup, + HttpResources, + HttpResourcesResponseOptions, + HttpResourcesRenderOptions, + HttpResourcesRequestHandler, + HttpResourcesServiceToolkit, +} from './types'; + +export interface SetupDeps { + http: InternalHttpServiceSetup; + rendering: InternalRenderingServiceSetup; +} + +export class HttpResourcesService implements CoreService { + private readonly logger: Logger; + constructor(core: CoreContext) { + this.logger = core.logger.get('http-resources'); + } + + setup(deps: SetupDeps) { + this.logger.debug('setting up HttpResourcesService'); + return { + createRegistrar: this.createRegistrar.bind(this, deps), + }; + } + + start() {} + stop() {} + + private createRegistrar(deps: SetupDeps, router: IRouter): HttpResources { + return { + register: ( + route: RouteConfig, + handler: HttpResourcesRequestHandler + ) => { + return router.get(route, (context, request, response) => { + return handler(context, request, { + ...response, + ...this.createResponseToolkit(deps, context, request, response), + }); + }); + }, + }; + } + + private createResponseToolkit( + deps: SetupDeps, + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ): HttpResourcesServiceToolkit { + const cspHeader = deps.http.csp.header; + return { + async renderCoreApp(options: HttpResourcesRenderOptions = {}) { + const body = await deps.rendering.render(request, context.core.uiSettings.client, { + includeUserSettings: true, + }); + + return response.ok({ + body, + headers: { ...options.headers, 'content-security-policy': cspHeader }, + }); + }, + async renderAnonymousCoreApp(options: HttpResourcesRenderOptions = {}) { + const body = await deps.rendering.render(request, context.core.uiSettings.client, { + includeUserSettings: false, + }); + + return response.ok({ + body, + headers: { ...options.headers, 'content-security-policy': cspHeader }, + }); + }, + renderHtml(options: HttpResourcesResponseOptions) { + return response.ok({ + body: options.body, + headers: { + ...options.headers, + 'content-type': 'text/html', + 'content-security-policy': cspHeader, + }, + }); + }, + renderJs(options: HttpResourcesResponseOptions) { + return response.ok({ + body: options.body, + headers: { + ...options.headers, + 'content-type': 'text/javascript', + 'content-security-policy': cspHeader, + }, + }); + }, + }; + } +} diff --git a/src/legacy/core_plugins/vis_type_timelion/public/flot.js b/src/core/server/http_resources/index.ts similarity index 75% rename from src/legacy/core_plugins/vis_type_timelion/public/flot.js rename to src/core/server/http_resources/index.ts index d6ca6d96c34ef..b373c6a9efa89 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/flot.js +++ b/src/core/server/http_resources/index.ts @@ -17,10 +17,13 @@ * under the License. */ -require('jquery.flot'); -require('jquery.flot.time'); -require('jquery.flot.symbol'); -require('jquery.flot.crosshair'); -require('jquery.flot.selection'); -require('jquery.flot.stack'); -require('jquery.flot.axislabels'); +export { HttpResourcesService } from './http_resources_service'; + +export { + HttpResourcesRenderOptions, + HttpResourcesResponseOptions, + HttpResourcesServiceToolkit, + HttpResourcesRequestHandler, + HttpResources, + InternalHttpResourcesSetup, +} from './types'; diff --git a/src/core/server/http_resources/integration_tests/http_resources_service.test.ts b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts new file mode 100644 index 0000000000000..0a5daa02e17e9 --- /dev/null +++ b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts @@ -0,0 +1,203 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +describe('http resources service', () => { + describe('register', () => { + let root: ReturnType; + const defaultCspRules = "script-src 'self'"; + beforeEach(async () => { + root = kbnTestServer.createRoot({ + csp: { + rules: [defaultCspRules], + }, + }); + }, 30000); + + afterEach(async () => { + await root.shutdown(); + }); + + describe('renderAnonymousCoreApp', () => { + it('renders core application', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + resources.register({ path: '/render-core', validate: false }, (context, req, res) => + res.renderAnonymousCoreApp() + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-core').expect(200); + + expect(response.text.length).toBeGreaterThan(0); + }); + + it('attaches CSP header', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + resources.register({ path: '/render-core', validate: false }, (context, req, res) => + res.renderAnonymousCoreApp() + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-core').expect(200); + + expect(response.header['content-security-policy']).toBe(defaultCspRules); + }); + + it('can attach headers, except the CSP header', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + resources.register({ path: '/render-core', validate: false }, (context, req, res) => + res.renderAnonymousCoreApp({ + headers: { + 'content-security-policy': "script-src 'unsafe-eval'", + 'x-kibana': '42', + }, + }) + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-core').expect(200); + + expect(response.header['content-security-policy']).toBe(defaultCspRules); + expect(response.header['x-kibana']).toBe('42'); + }); + }); + + describe('custom renders', () => { + it('renders html', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + const htmlBody = ` + + + +

HTML body

+ + + `; + resources.register({ path: '/render-html', validate: false }, (context, req, res) => + res.renderHtml({ body: htmlBody }) + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-html').expect(200); + + expect(response.text).toBe(htmlBody); + expect(response.header['content-type']).toBe('text/html; charset=utf-8'); + }); + + it('renders javascript', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + const jsBody = 'window.alert("from js body");'; + resources.register({ path: '/render-js', validate: false }, (context, req, res) => + res.renderJs({ body: jsBody }) + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-js').expect(200); + + expect(response.text).toBe(jsBody); + expect(response.header['content-type']).toBe('text/javascript; charset=utf-8'); + }); + + it('attaches CSP header', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + const htmlBody = ` + + + +

HTML body

+ + + `; + resources.register({ path: '/render-html', validate: false }, (context, req, res) => + res.renderHtml({ body: htmlBody }) + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-html').expect(200); + + expect(response.header['content-security-policy']).toBe(defaultCspRules); + }); + + it('can attach headers, except the CSP & "content-type" headers', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + resources.register({ path: '/render-core', validate: false }, (context, req, res) => + res.renderHtml({ + body: '

Hi

', + headers: { + 'content-security-policy': "script-src 'unsafe-eval'", + 'content-type': 'text/html', + 'x-kibana': '42', + }, + }) + ); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/render-core').expect(200); + + expect(response.header['content-security-policy']).toBe(defaultCspRules); + expect(response.header['x-kibana']).toBe('42'); + }); + + it('can adjust route config', async () => { + const { http, httpResources } = await root.setup(); + + const router = http.createRouter(''); + const resources = httpResources.createRegistrar(router); + const validate = { + params: schema.object({ + id: schema.string(), + }), + }; + + resources.register({ path: '/render-js-with-param/{id}', validate }, (context, req, res) => + res.renderJs({ body: `window.alert(${req.params.id});` }) + ); + + await root.start(); + const response = await kbnTestServer.request + .get(root, '/render-js-with-param/42') + .expect(200); + + expect(response.text).toBe('window.alert(42);'); + }); + }); + }); +}); diff --git a/src/core/server/http_resources/types.ts b/src/core/server/http_resources/types.ts new file mode 100644 index 0000000000000..d761e2def1023 --- /dev/null +++ b/src/core/server/http_resources/types.ts @@ -0,0 +1,116 @@ +/* + * 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 { + IRouter, + RouteConfig, + IKibanaResponse, + ResponseHeaders, + HttpResponseOptions, + KibanaResponseFactory, + RequestHandler, +} from '../http'; + +/** + * Allows to configure HTTP response parameters + * @public + */ +export interface HttpResourcesRenderOptions { + /** + * HTTP Headers with additional information about response. + * @remarks + * All HTML pages are already pre-configured with `content-security-policy` header that cannot be overridden. + * */ + headers?: ResponseHeaders; +} + +/** + * HTTP Resources response parameters + * @public + */ +export type HttpResourcesResponseOptions = HttpResponseOptions; + +/** + * Extended set of {@link KibanaResponseFactory} helpers used to respond with HTML or JS resource. + * @public + */ +export interface HttpResourcesServiceToolkit { + /** To respond with HTML page bootstrapping Kibana application. */ + renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise; + /** To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. */ + renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise; + /** To respond with a custom HTML page. */ + renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse; + /** To respond with a custom JS script file. */ + renderJs: (options: HttpResourcesResponseOptions) => IKibanaResponse; +} + +/** + * Extended version of {@link RequestHandler} having access to {@link HttpResourcesServiceToolkit} + * to respond with HTML or JS resources. + * @param context {@link RequestHandlerContext} - the core context exposed for this request. + * @param request {@link KibanaRequest} - object containing information about requested resource, + * such as path, method, headers, parameters, query, body, etc. + * @param response {@link KibanaResponseFactory} {@libk HttpResourcesServiceToolkit} - a set of helper functions used to respond to a request. + * + * @example + * ```typescript + * httpResources.register({ + * path: '/login', + * validate: { + * params: schema.object({ id: schema.string() }), + * }, + * }, + * async (context, request, response) => { + * //.. + * return response.renderCoreApp(); + * }); + * @public + */ +export type HttpResourcesRequestHandler

= RequestHandler< + P, + Q, + B, + 'get', + KibanaResponseFactory & HttpResourcesServiceToolkit +>; + +/** + * Allows to configure HTTP response parameters + * @internal + */ +export interface InternalHttpResourcesSetup { + createRegistrar(router: IRouter): HttpResources; +} + +/** + * HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. + * Provides API allowing plug-ins to respond with: + * - a pre-configured HTML page bootstrapping Kibana client app + * - custom HTML page + * - custom JS script file. + * @public + */ +export interface HttpResources { + /** To register a route handler executing passed function to form response. */ + register: ( + route: RouteConfig, + handler: HttpResourcesRequestHandler + ) => void; +} diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 039988fa08968..ef57fae159d7e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -47,7 +47,8 @@ import { } from './elasticsearch'; import { HttpServiceSetup } from './http'; -import { IScopedRenderingClient } from './rendering'; +import { HttpResources } from './http_resources'; + import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; import { ContextSetup } from './context'; import { IUiSettingsClient, UiSettingsServiceSetup, UiSettingsServiceStart } from './ui_settings'; @@ -146,6 +147,7 @@ export { OnPreResponseInfo, RedirectResponseOptions, RequestHandler, + RequestHandlerWrapper, RequestHandlerContextContainer, RequestHandlerContextProvider, ResponseError, @@ -175,7 +177,15 @@ export { DestructiveRouteMethod, SafeRouteMethod, } from './http'; -export { RenderingServiceSetup, IRenderOptions } from './rendering'; + +export { + HttpResourcesRenderOptions, + HttpResourcesResponseOptions, + HttpResourcesServiceToolkit, + HttpResourcesRequestHandler, +} from './http_resources'; + +export { IRenderOptions } from './rendering'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; export { @@ -313,8 +323,6 @@ export { * Plugin specific context passed to a route handler. * * Provides the following clients and services: - * - {@link IScopedRenderingClient | rendering} - Rendering client - * which uses the data of the incoming request * - {@link SavedObjectsClient | savedObjects.client} - Saved Objects client * which uses the credentials of the incoming request * - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing @@ -330,7 +338,6 @@ export { */ export interface RequestHandlerContext { core: { - rendering: IScopedRenderingClient; savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; @@ -362,7 +369,10 @@ export interface CoreSetup ({ +jest.doMock('./plugins/find_legacy_plugin_specs', () => ({ findLegacyPluginSpecs: findLegacyPluginSpecsMock, })); + +export const logLegacyThirdPartyPluginDeprecationWarningMock = jest.fn(); +jest.doMock('./plugins/log_legacy_plugins_warning', () => ({ + logLegacyThirdPartyPluginDeprecationWarning: logLegacyThirdPartyPluginDeprecationWarningMock, +})); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 0cf2ebe55ea10..a75f7dda302c2 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -22,7 +22,10 @@ jest.mock('../../../cli/cluster/cluster_manager'); jest.mock('./config/legacy_deprecation_adapters', () => ({ convertLegacyDeprecationProvider: (provider: any) => Promise.resolve(provider), })); -import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks'; +import { + findLegacyPluginSpecsMock, + logLegacyThirdPartyPluginDeprecationWarningMock, +} from './legacy_service.test.mocks'; import { BehaviorSubject, throwError } from 'rxjs'; @@ -41,6 +44,7 @@ import { httpServiceMock } from '../http/http_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; +import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; import { uuidServiceMock } from '../uuid/uuid_service.mock'; import { metricsServiceMock } from '../metrics/metrics_service.mock'; @@ -86,23 +90,11 @@ beforeEach(() => { getAuthHeaders: () => undefined, } as any, }, + httpResources: httpResourcesMock.createSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), plugins: { initialized: true, contracts: new Map([['plugin-id', 'plugin-value']]), - uiPlugins: { - public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([ - [ - 'plugin-id', - { - publicTargetDir: 'path/to/target/public', - publicAssetsDir: '/plugins/name/assets/', - }, - ], - ]), - browserConfigs: new Map(), - }, }, rendering: renderingServiceMock, metrics: metricsServiceMock.createInternalSetupContract(), @@ -110,6 +102,19 @@ beforeEach(() => { status: statusServiceMock.createInternalSetupContract(), }, plugins: { 'plugin-id': 'plugin-value' }, + uiPlugins: { + public: new Map([['plugin-id', {} as DiscoveredPlugin]]), + internal: new Map([ + [ + 'plugin-id', + { + publicTargetDir: 'path/to/target/public', + publicAssetsDir: '/plugins/name/assets/', + }, + ], + ]), + browserConfigs: new Map(), + }, }; startDeps = { @@ -474,6 +479,38 @@ describe('#discoverPlugins()', () => { expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerA'); expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerB'); }); + + it(`logs deprecations for legacy third party plugins`, async () => { + const pluginSpecs = [ + { getId: () => 'pluginA', getDeprecationsProvider: () => undefined }, + { getId: () => 'pluginB', getDeprecationsProvider: () => undefined }, + ]; + findLegacyPluginSpecsMock.mockImplementation( + settings => + Promise.resolve({ + pluginSpecs, + pluginExtendedConfig: settings, + disabledPluginSpecs: [], + uiExports: {}, + navLinks: [], + }) as any + ); + + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); + + await legacyService.discoverPlugins(); + + expect(logLegacyThirdPartyPluginDeprecationWarningMock).toHaveBeenCalledTimes(1); + expect(logLegacyThirdPartyPluginDeprecationWarningMock).toHaveBeenCalledWith({ + specs: pluginSpecs, + log: expect.any(Object), + }); + }); }); test('Sets the server.uuid property on the legacy configuration', async () => { diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index f77230301ce02..b95362e1ea26e 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -28,7 +28,7 @@ import { DevConfig, DevConfigType, config as devConfig } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig } from '../http'; import { Logger } from '../logging'; import { PathConfigType } from '../path'; -import { findLegacyPluginSpecs } from './plugins'; +import { findLegacyPluginSpecs, logLegacyThirdPartyPluginDeprecationWarning } from './plugins'; import { convertLegacyDeprecationProvider } from './config'; import { ILegacyInternals, @@ -133,6 +133,11 @@ export class LegacyService implements CoreService { this.coreContext.env.packageInfo ); + logLegacyThirdPartyPluginDeprecationWarning({ + specs: pluginSpecs, + log: this.log, + }); + this.legacyPlugins = { pluginSpecs, disabledPluginSpecs, @@ -269,6 +274,7 @@ export class LegacyService implements CoreService { uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, }; + const router = setupDeps.core.http.createRouter('', this.legacyId); const coreSetup: CoreSetup = { capabilities: setupDeps.core.capabilities, context: setupDeps.core.context, @@ -283,7 +289,8 @@ export class LegacyService implements CoreService { null, this.legacyId ), - createRouter: () => setupDeps.core.http.createRouter('', this.legacyId), + createRouter: () => router, + resources: setupDeps.core.httpResources.createRegistrar(router), registerOnPreAuth: setupDeps.core.http.registerOnPreAuth, registerAuth: setupDeps.core.http.registerAuth, registerOnPostAuth: setupDeps.core.http.registerOnPostAuth, @@ -342,7 +349,7 @@ export class LegacyService implements CoreService { }, hapiServer: setupDeps.core.http.server, kibanaMigrator: startDeps.core.savedObjects.migrator, - uiPlugins: setupDeps.core.plugins.uiPlugins, + uiPlugins: setupDeps.uiPlugins, elasticsearch: setupDeps.core.elasticsearch, rendering: setupDeps.core.rendering, uiSettings: setupDeps.core.uiSettings, diff --git a/src/core/server/legacy/plugins/index.ts b/src/core/server/legacy/plugins/index.ts index a6d55e1da7839..7ec5dbc1983ab 100644 --- a/src/core/server/legacy/plugins/index.ts +++ b/src/core/server/legacy/plugins/index.ts @@ -18,3 +18,4 @@ */ export { findLegacyPluginSpecs } from './find_legacy_plugin_specs'; +export { logLegacyThirdPartyPluginDeprecationWarning } from './log_legacy_plugins_warning'; diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts new file mode 100644 index 0000000000000..1790b096a71ae --- /dev/null +++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { loggerMock } from '../../logging/logger.mock'; +import { logLegacyThirdPartyPluginDeprecationWarning } from './log_legacy_plugins_warning'; +import { LegacyPluginSpec } from '../types'; + +const createPluginSpec = ({ id, path }: { id: string; path: string }): LegacyPluginSpec => { + return { + getId: () => id, + getExpectedKibanaVersion: () => 'kibana', + getConfigPrefix: () => 'plugin.config', + getDeprecationsProvider: () => undefined, + getPack: () => ({ + getPath: () => path, + }), + }; +}; + +describe('logLegacyThirdPartyPluginDeprecationWarning', () => { + let log: ReturnType; + + beforeEach(() => { + log = loggerMock.create(); + }); + + it('logs warning for third party plugins', () => { + logLegacyThirdPartyPluginDeprecationWarning({ + specs: [createPluginSpec({ id: 'plugin', path: '/some-external-path' })], + log, + }); + expect(log.warn).toHaveBeenCalledTimes(1); + expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Some installed third party plugin(s) [plugin] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html for a list of breaking changes and https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md for documentation on how to migrate legacy plugins.", + ] + `); + }); + + it('lists all the deprecated plugins and only log once', () => { + logLegacyThirdPartyPluginDeprecationWarning({ + specs: [ + createPluginSpec({ id: 'pluginA', path: '/abs/path/to/pluginA' }), + createPluginSpec({ id: 'pluginB', path: '/abs/path/to/pluginB' }), + createPluginSpec({ id: 'pluginC', path: '/abs/path/to/pluginC' }), + ], + log, + }); + expect(log.warn).toHaveBeenCalledTimes(1); + expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Some installed third party plugin(s) [pluginA, pluginB, pluginC] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html for a list of breaking changes and https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md for documentation on how to migrate legacy plugins.", + ] + `); + }); + + it('does not log warning for internal legacy plugins', () => { + logLegacyThirdPartyPluginDeprecationWarning({ + specs: [ + createPluginSpec({ + id: 'plugin', + path: '/absolute/path/to/kibana/src/legacy/core_plugins', + }), + createPluginSpec({ + id: 'plugin', + path: '/absolute/path/to/kibana/x-pack', + }), + ], + log, + }); + + expect(log.warn).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts new file mode 100644 index 0000000000000..f9c3dcbf554cb --- /dev/null +++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts @@ -0,0 +1,52 @@ +/* + * 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 { Logger } from '../../logging'; +import { LegacyPluginSpec } from '../types'; + +const internalPaths = ['/src/legacy/core_plugins', '/x-pack']; + +const breakingChangesUrl = + 'https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html'; +const migrationGuideUrl = 'https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md'; + +export const logLegacyThirdPartyPluginDeprecationWarning = ({ + specs, + log, +}: { + specs: LegacyPluginSpec[]; + log: Logger; +}) => { + const thirdPartySpecs = specs.filter(isThirdPartyPluginSpec); + if (thirdPartySpecs.length > 0) { + const pluginIds = thirdPartySpecs.map(spec => spec.getId()); + log.warn( + `Some installed third party plugin(s) [${pluginIds.join( + ', ' + )}] are using the legacy plugin format and will no longer work in a future Kibana release. ` + + `Please refer to ${breakingChangesUrl} for a list of breaking changes ` + + `and ${migrationGuideUrl} for documentation on how to migrate legacy plugins.` + ); + } +}; + +const isThirdPartyPluginSpec = (spec: LegacyPluginSpec): boolean => { + const pluginPath = spec.getPack().getPath(); + return !internalPaths.some(internalPath => pluginPath.indexOf(internalPath) > -1); +}; diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index 0c1a7730f92a7..2567ca790e04f 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -22,8 +22,8 @@ import { Server } from 'hapi'; import { ChromeNavLink } from '../../public'; import { KibanaRequest, LegacyRequest } from '../http'; import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; -import { PluginsServiceSetup, PluginsServiceStart } from '../plugins'; -import { RenderingServiceSetup } from '../rendering'; +import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins'; +import { InternalRenderingServiceSetup } from '../rendering'; import { SavedObjectsLegacyUiExports } from '../types'; /** @@ -34,7 +34,7 @@ export type LegacyVars = Record; type LegacyCoreSetup = InternalCoreSetup & { plugins: PluginsServiceSetup; - rendering: RenderingServiceSetup; + rendering: InternalRenderingServiceSetup; }; type LegacyCoreStart = InternalCoreStart & { plugins: PluginsServiceStart }; @@ -98,6 +98,7 @@ export interface LegacyPluginSpec { getExpectedKibanaVersion: () => string; getConfigPrefix: () => string; getDeprecationsProvider: () => LegacyConfigDeprecationProvider | undefined; + getPack: () => LegacyPluginPack; } /** @@ -173,6 +174,7 @@ export type LegacyUiExports = SavedObjectsLegacyUiExports & { export interface LegacyServiceSetupDeps { core: LegacyCoreSetup; plugins: Record; + uiPlugins: UiPlugins; } /** diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index faf73044cac4d..3b9a39db72278 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -23,10 +23,12 @@ import { CspConfig } from './csp'; import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; import { httpServiceMock } from './http/http_service.mock'; +import { httpResourcesMock } from './http_resources/http_resources_service.mock'; import { contextServiceMock } from './context/context_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { savedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock'; import { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; +import { renderingMock } from './rendering/rendering_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { SharedGlobalConfig } from './plugins'; import { InternalCoreSetup, InternalCoreStart } from './internal_types'; @@ -36,6 +38,7 @@ import { uuidServiceMock } from './uuid/uuid_service.mock'; import { statusServiceMock } from './status/status_service.mock'; export { httpServerMock } from './http/http_server.mocks'; +export { httpResourcesMock } from './http_resources/http_resources_service.mock'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; export { configServiceMock } from './config/config_service.mock'; export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; @@ -45,6 +48,7 @@ export { savedObjectsRepositoryMock } from './saved_objects/service/lib/reposito export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { metricsServiceMock } from './metrics/metrics_service.mock'; +export { renderingMock } from './rendering/rendering_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -120,6 +124,7 @@ function createCoreSetupMock({ get: httpService.auth.get, isAuthenticated: httpService.auth.isAuthenticated, }, + resources: httpResourcesMock.createRegistrar(), getServerInfo: httpService.getServerInfo, }; httpMock.createRouter.mockImplementation(() => httpService.createRouter('')); @@ -167,6 +172,8 @@ function createInternalCoreSetupMock() { savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), + httpResources: httpResourcesMock.createSetupContract(), + rendering: renderingMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), }; return setupDeps; @@ -184,9 +191,6 @@ function createInternalCoreStartMock() { function createCoreRequestHandlerContextMock() { return { - rendering: { - render: jest.fn(), - }, savedObjects: { client: savedObjectsClientMock.create(), typeRegistry: savedObjectsTypeRegistryMock.create(), diff --git a/src/core/server/plugins/index.ts b/src/core/server/plugins/index.ts index c7ef213c8f187..e480de750bb1a 100644 --- a/src/core/server/plugins/index.ts +++ b/src/core/server/plugins/index.ts @@ -17,7 +17,12 @@ * under the License. */ -export { PluginsService, PluginsServiceSetup, PluginsServiceStart } from './plugins_service'; +export { + PluginsService, + PluginsServiceSetup, + PluginsServiceStart, + UiPlugins, +} from './plugins_service'; export { config } from './plugins_config'; /** @internal */ export { isNewPlatformPlugin } from './discovery'; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 61d97aea97459..ab18a9cbbc062 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -136,6 +136,8 @@ export function createPluginSetupContext( deps: PluginsServiceSetupDeps, plugin: PluginWrapper ): CoreSetup { + const router = deps.http.createRouter('', plugin.opaqueId); + return { capabilities: { registerProvider: deps.capabilities.registerProvider, @@ -155,7 +157,8 @@ export function createPluginSetupContext( null, plugin.opaqueId ), - createRouter: () => deps.http.createRouter('', plugin.opaqueId), + createRouter: () => router, + resources: deps.httpResources.createRegistrar(router), registerOnPreAuth: deps.http.registerOnPreAuth, registerAuth: deps.http.registerAuth, registerOnPostAuth: deps.http.registerOnPostAuth, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index 29e5b83b2e4c7..a40566767ddae 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -23,14 +23,10 @@ type PluginsServiceMock = jest.Mocked>; const createSetupContractMock = (): PluginsServiceSetup => ({ contracts: new Map(), - uiPlugins: { - browserConfigs: new Map(), - internal: new Map(), - public: new Map(), - }, initialized: true, }); const createStartContractMock = () => ({ contracts: new Map() }); + const createServiceMock = (): PluginsServiceMock => ({ discover: jest.fn(), setup: jest.fn().mockResolvedValue(createSetupContractMock()), @@ -38,8 +34,17 @@ const createServiceMock = (): PluginsServiceMock => ({ stop: jest.fn(), }); +function createUiPlugins() { + return { + browserConfigs: new Map(), + internal: new Map(), + public: new Map(), + }; +} + export const pluginServiceMock = { create: createServiceMock, createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, + createUiPlugins, }; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 14147ab9f2a8d..38fda12bd290f 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -120,6 +120,7 @@ describe('PluginsService', () => { pluginsService = new PluginsService({ coreId, env, logger, configService }); [mockPluginSystem] = MockPluginsSystem.mock.instances as any; + mockPluginSystem.uiPlugins.mockReturnValue(new Map()); }); afterEach(() => { @@ -202,7 +203,6 @@ describe('PluginsService', () => { .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); mockPluginSystem.setupPlugins.mockResolvedValue(new Map()); - mockPluginSystem.uiPlugins.mockReturnValue(new Map()); mockDiscover.mockReturnValue({ error$: from([]), @@ -234,8 +234,6 @@ describe('PluginsService', () => { const setup = await pluginsService.setup(setupDeps); expect(setup.contracts).toBeInstanceOf(Map); - expect(setup.uiPlugins.public).toBeInstanceOf(Map); - expect(setup.uiPlugins.internal).toBeInstanceOf(Map); expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); @@ -273,7 +271,8 @@ describe('PluginsService', () => { plugin$: from([firstPlugin, secondPlugin]), }); - await expect(pluginsService.discover()).resolves.toBeUndefined(); + const { pluginTree } = await pluginsService.discover(); + expect(pluginTree).toBeUndefined(); expect(mockDiscover).toHaveBeenCalledTimes(1); expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); @@ -308,7 +307,8 @@ describe('PluginsService', () => { plugin$: from([firstPlugin, secondPlugin, thirdPlugin, lastPlugin, missingDepsPlugin]), }); - await expect(pluginsService.discover()).resolves.toBeUndefined(); + const { pluginTree } = await pluginsService.discover(); + expect(pluginTree).toBeUndefined(); expect(mockDiscover).toHaveBeenCalledTimes(1); expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(4); @@ -466,12 +466,8 @@ describe('PluginsService', () => { }); mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); - await pluginsService.discover(); - const { - uiPlugins: { browserConfigs }, - } = await pluginsService.setup(setupDeps); - - const uiConfig$ = browserConfigs.get('plugin-with-expose'); + const { uiPlugins } = await pluginsService.discover(); + const uiConfig$ = uiPlugins.browserConfigs.get('plugin-with-expose'); expect(uiConfig$).toBeDefined(); const uiConfig = await uiConfig$!.pipe(take(1)).toPromise(); @@ -506,12 +502,8 @@ describe('PluginsService', () => { }); mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); - await pluginsService.discover(); - const { - uiPlugins: { browserConfigs }, - } = await pluginsService.setup(setupDeps); - - expect([...browserConfigs.entries()]).toHaveLength(0); + const { uiPlugins } = await pluginsService.discover(); + expect([...uiPlugins.browserConfigs.entries()]).toHaveLength(0); }); }); @@ -539,8 +531,7 @@ describe('PluginsService', () => { describe('uiPlugins.internal', () => { it('includes disabled plugins', async () => { config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); - await pluginsService.discover(); - const { uiPlugins } = await pluginsService.setup(setupDeps); + const { uiPlugins } = await pluginsService.discover(); expect(uiPlugins.internal).toMatchInlineSnapshot(` Map { "plugin-1" => Object { diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index a0ecee47c675f..d7a348affe94f 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -39,23 +39,25 @@ export interface PluginsServiceSetup { initialized: boolean; /** Setup contracts returned by plugins. */ contracts: Map; - uiPlugins: { - /** - * Paths to all discovered ui plugin entrypoints on the filesystem, even if - * disabled. - */ - internal: Map; - - /** - * Information needed by client-side to load plugins and wire dependencies. - */ - public: Map; - - /** - * Configuration for plugins to be exposed to the client-side. - */ - browserConfigs: Map>; - }; +} + +/** @internal */ +export interface UiPlugins { + /** + * Paths to all discovered ui plugin entrypoints on the filesystem, even if + * disabled. + */ + internal: Map; + + /** + * Information needed by client-side to load plugins and wire dependencies. + */ + public: Map; + + /** + * Configuration for plugins to be exposed to the client-side. + */ + browserConfigs: Map>; } /** @internal */ @@ -97,8 +99,17 @@ export class PluginsService implements CoreService; -export const setupMock: jest.Mocked = { +export const setupMock: jest.Mocked = { render: jest.fn(), }; export const mockSetup = jest.fn().mockResolvedValue(setupMock); diff --git a/src/core/server/rendering/rendering_service.mock.ts b/src/core/server/rendering/rendering_service.mock.ts new file mode 100644 index 0000000000000..7eba332512386 --- /dev/null +++ b/src/core/server/rendering/rendering_service.mock.ts @@ -0,0 +1,31 @@ +/* + * 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 { InternalRenderingServiceSetup } from './types'; + +function createRenderingSetup() { + const mocked: jest.Mocked = { + render: jest.fn().mockResolvedValue(''), + }; + return mocked; +} + +export const renderingMock = { + createSetupContract: createRenderingSetup, +}; diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index 43ff4f633085c..d1c527aca4dba 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -22,7 +22,7 @@ import { load } from 'cheerio'; import { httpServerMock } from '../http/http_server.mocks'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { mockRenderingServiceParams, mockRenderingSetupDeps } from './__mocks__/params'; -import { RenderingServiceSetup } from './types'; +import { InternalRenderingServiceSetup } from './types'; import { RenderingService } from './rendering_service'; const INJECTED_METADATA = { @@ -62,15 +62,9 @@ describe('RenderingService', () => { }); describe('setup()', () => { - it('creates instance of RenderingServiceSetup', async () => { - const rendering = await service.setup(mockRenderingSetupDeps); - - expect(rendering.render).toBeInstanceOf(Function); - }); - describe('render()', () => { let uiSettings: ReturnType; - let render: RenderingServiceSetup['render']; + let render: InternalRenderingServiceSetup['render']; beforeEach(async () => { uiSettings = uiSettingsServiceMock.createClient(); @@ -78,6 +72,13 @@ describe('RenderingService', () => { registered: { name: 'title' }, }); render = (await service.setup(mockRenderingSetupDeps)).render; + await service.start({ + legacy: { + legacyInternals: { + getVars: () => ({}), + }, + }, + } as any); }); it('renders "core" page', async () => { diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index dbafd5806bd74..a02d85d22b2cb 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -23,41 +23,37 @@ import { take } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; +import { UiPlugins } from '../plugins'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Template } from './views'; +import { LegacyService } from '../legacy'; import { IRenderOptions, RenderingSetupDeps, - RenderingServiceSetup, + InternalRenderingServiceSetup, RenderingMetadata, } from './types'; /** @internal */ -export class RenderingService implements CoreService { +export class RenderingService implements CoreService { + private legacyInternals?: LegacyService['legacyInternals']; constructor(private readonly coreContext: CoreContext) {} public async setup({ http, legacyPlugins, - plugins, - }: RenderingSetupDeps): Promise { - async function getUiConfig(pluginId: string) { - const browserConfig = plugins.uiPlugins.browserConfigs.get(pluginId); - - return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record; - } - + uiPlugins, + }: RenderingSetupDeps): Promise { return { render: async ( request, uiSettings, - { - app = { getId: () => 'core' }, - includeUserSettings = true, - vars = {}, - }: IRenderOptions = {} + { app = { getId: () => 'core' }, includeUserSettings = true, vars }: IRenderOptions = {} ) => { + if (!this.legacyInternals) { + throw new Error('Cannot render before "start"'); + } const { env } = this.coreContext; const basePath = http.basePath.get(request); const serverBasePath = http.basePath.serverBasePath; @@ -87,12 +83,12 @@ export class RenderingService implements CoreService { translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, }, csp: { warnLegacyBrowsers: http.csp.warnLegacyBrowsers }, - vars, + vars: vars ?? (await this.legacyInternals!.getVars('core', request)), uiPlugins: await Promise.all( - [...plugins.uiPlugins.public].map(async ([id, plugin]) => ({ + [...uiPlugins.public].map(async ([id, plugin]) => ({ id, plugin, - config: await getUiConfig(id), + config: await this.getUiConfig(uiPlugins, id), })) ), legacyMetadata: { @@ -116,7 +112,15 @@ export class RenderingService implements CoreService { }; } - public async start() {} + public async start({ legacy }: { legacy: LegacyService }) { + this.legacyInternals = legacy.legacyInternals; + } public async stop() {} + + private async getUiConfig(uiPlugins: UiPlugins, pluginId: string) { + const browserConfig = uiPlugins.browserConfigs.get(pluginId); + + return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record; + } } diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index cfaa23d491139..2a3be93055006 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -23,7 +23,7 @@ import { Env } from '../config'; import { ICspConfig } from '../csp'; import { InternalHttpServiceSetup, KibanaRequest, LegacyRequest } from '../http'; import { LegacyNavLink, LegacyServiceDiscoverPlugins } from '../legacy'; -import { PluginsServiceSetup, DiscoveredPlugin } from '../plugins'; +import { UiPlugins, DiscoveredPlugin } from '../plugins'; import { IUiSettingsClient, UserProvidedValues } from '../ui_settings'; /** @internal */ @@ -75,7 +75,7 @@ export interface RenderingMetadata { export interface RenderingSetupDeps { http: InternalHttpServiceSetup; legacyPlugins: LegacyServiceDiscoverPlugins; - plugins: PluginsServiceSetup; + uiPlugins: UiPlugins; } /** @public */ @@ -102,31 +102,8 @@ export interface IRenderOptions { vars?: Record; } -/** @public */ -export interface IScopedRenderingClient { - /** - * Generate a `KibanaResponse` which renders an HTML page bootstrapped - * with the `core` bundle. Intended as a response body for HTTP route handlers. - * - * @example - * ```ts - * router.get( - * { path: '/', validate: false }, - * (context, request, response) => - * response.ok({ - * body: await context.core.rendering.render(), - * headers: { - * 'content-security-policy': context.core.http.csp.header, - * }, - * }) - * ); - * ``` - */ - render(options?: Pick): Promise; -} - /** @internal */ -export interface RenderingServiceSetup { +export interface InternalRenderingServiceSetup { /** * Generate a `KibanaResponse` which renders an HTML page bootstrapped * with the `core` bundle or the ID of another specified legacy bundle. diff --git a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts index 44add4e977006..2c710d4eaa079 100644 --- a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts +++ b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts @@ -26,7 +26,7 @@ const createRegistry = (...types: Array>) => { types.forEach(type => registry.registerType({ name: 'unknown', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, migrations: {}, @@ -41,7 +41,7 @@ test('mappings without index pattern goes to default index', () => { kibanaIndexName: '.kibana', registry: createRegistry({ name: 'type1', - namespaceAgnostic: false, + namespaceType: 'single', }), indexMap: { type1: { @@ -73,7 +73,7 @@ test(`mappings with custom index pattern doesn't go to default index`, () => { kibanaIndexName: '.kibana', registry: createRegistry({ name: 'type1', - namespaceAgnostic: false, + namespaceType: 'single', indexPattern: '.other_kibana', }), indexMap: { @@ -106,7 +106,7 @@ test('creating a script gets added to the index pattern', () => { kibanaIndexName: '.kibana', registry: createRegistry({ name: 'type1', - namespaceAgnostic: false, + namespaceType: 'single', indexPattern: '.other_kibana', convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, }), @@ -141,12 +141,12 @@ test('throws when two scripts are defined for an index pattern', () => { const registry = createRegistry( { name: 'type1', - namespaceAgnostic: false, + namespaceType: 'single', convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, }, { name: 'type2', - namespaceAgnostic: false, + namespaceType: 'single', convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, } ); diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index ef3f546b5e574..64270c677ff20 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -32,7 +32,7 @@ const createRegistry = (...types: Array>) => { types.forEach(type => registry.registerType({ name: 'unknown', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, migrations: {}, diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts index 257b32c1e4c23..3f5c0c3876615 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -27,7 +27,7 @@ const defaultSavedObjectTypes: SavedObjectsType[] = [ { name: 'testtype', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { name: { type: 'keyword' }, diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 336eeff99f47b..cda0e86f15bdf 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -28,8 +28,8 @@ const createRegistry = (types: Array>) => { types.forEach(type => registry.registerType({ name: 'unknown', - namespaceAgnostic: false, hidden: false, + namespaceType: 'single', mappings: { properties: {} }, migrations: {}, ...type, @@ -120,7 +120,7 @@ function mockOptions(): KibanaMigratorOptions { { name: 'testtype', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { name: { type: 'keyword' }, @@ -131,7 +131,7 @@ function mockOptions(): KibanaMigratorOptions { { name: 'testtype2', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', indexPattern: 'other-index', mappings: { properties: { diff --git a/src/core/server/saved_objects/routes/integration_tests/test_utils.ts b/src/core/server/saved_objects/routes/integration_tests/test_utils.ts index 82a889f75d3c1..23e0285201dc7 100644 --- a/src/core/server/saved_objects/routes/integration_tests/test_utils.ts +++ b/src/core/server/saved_objects/routes/integration_tests/test_utils.ts @@ -49,7 +49,7 @@ export const createExportableType = (name: string): SavedObjectsType => { return { name, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: {}, }, diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts index 84337474f3ee3..f82822f90f489 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts @@ -183,14 +183,6 @@ describe('SavedObjectTypeRegistry', () => { expectResult(false, { namespaceType: 'single' }); expectResult(false, { namespaceType: undefined }); }); - - // deprecated test cases - it(`returns true when namespaceAgnostic is true`, () => { - expectResult(true, { namespaceAgnostic: true, namespaceType: 'agnostic' }); - expectResult(true, { namespaceAgnostic: true, namespaceType: 'multiple' }); - expectResult(true, { namespaceAgnostic: true, namespaceType: 'single' }); - expectResult(true, { namespaceAgnostic: true, namespaceType: undefined }); - }); }); describe('#isSingleNamespace', () => { @@ -213,14 +205,6 @@ describe('SavedObjectTypeRegistry', () => { expectResult(false, { namespaceType: 'agnostic' }); expectResult(false, { namespaceType: 'multiple' }); }); - - // deprecated test cases - it(`returns false when namespaceAgnostic is true`, () => { - expectResult(false, { namespaceAgnostic: true, namespaceType: 'agnostic' }); - expectResult(false, { namespaceAgnostic: true, namespaceType: 'multiple' }); - expectResult(false, { namespaceAgnostic: true, namespaceType: 'single' }); - expectResult(false, { namespaceAgnostic: true, namespaceType: undefined }); - }); }); describe('#isMultiNamespace', () => { @@ -243,14 +227,6 @@ describe('SavedObjectTypeRegistry', () => { expectResult(false, { namespaceType: 'single' }); expectResult(false, { namespaceType: undefined }); }); - - // deprecated test cases - it(`returns false when namespaceAgnostic is true`, () => { - expectResult(false, { namespaceAgnostic: true, namespaceType: 'agnostic' }); - expectResult(false, { namespaceAgnostic: true, namespaceType: 'multiple' }); - expectResult(false, { namespaceAgnostic: true, namespaceType: 'single' }); - expectResult(false, { namespaceAgnostic: true, namespaceType: undefined }); - }); }); describe('#isHidden', () => { diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts index be3fdb86a994c..740313a53d1e2 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.ts @@ -72,11 +72,7 @@ export class SavedObjectTypeRegistry { * resolves to `false` if the type is not registered */ public isNamespaceAgnostic(type: string) { - return ( - this.types.get(type)?.namespaceType === 'agnostic' || - this.types.get(type)?.namespaceAgnostic || - false - ); + return this.types.get(type)?.namespaceType === 'agnostic'; } /** @@ -84,6 +80,7 @@ export class SavedObjectTypeRegistry { * resolves to `true` if the type is not registered */ public isSingleNamespace(type: string) { + // in the case we somehow registered a type with an invalid `namespaceType`, treat it as single-namespace return !this.isNamespaceAgnostic(type) && !this.isMultiNamespace(type); } @@ -92,7 +89,7 @@ export class SavedObjectTypeRegistry { * resolves to `false` if the type is not registered */ public isMultiNamespace(type: string) { - return !this.isNamespaceAgnostic(type) && this.types.get(type)?.namespaceType === 'multiple'; + return this.types.get(type)?.namespaceType === 'multiple'; } /** diff --git a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts index a6b580e9b3461..ea881805e1ae6 100644 --- a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts @@ -32,7 +32,7 @@ describe('SavedObjectsRepository#createRepository', () => { typeRegistry.registerType({ name: 'nsAgnosticType', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { name: { type: 'keyword' }, @@ -44,7 +44,7 @@ describe('SavedObjectsRepository#createRepository', () => { typeRegistry.registerType({ name: 'nsType', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', indexPattern: 'beats', mappings: { properties: { @@ -56,7 +56,7 @@ describe('SavedObjectsRepository#createRepository', () => { typeRegistry.registerType({ name: 'hiddenType', hidden: true, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { name: { type: 'keyword' }, diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 9efc82603b179..b50c6dc9a1abf 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -202,15 +202,10 @@ export interface SavedObjectsType { * See {@link SavedObjectsServiceStart.createInternalRepository | createInternalRepository}. */ hidden: boolean; - /** - * Is the type global (true), or not (false). - * @deprecated Use `namespaceType` instead. - */ - namespaceAgnostic?: boolean; /** * The {@link SavedObjectsNamespaceType | namespace type} for the type. */ - namespaceType?: SavedObjectsNamespaceType; + namespaceType: SavedObjectsNamespaceType; /** * If defined, the type instances will be stored in the given index instead of the default one. */ diff --git a/src/core/server/saved_objects/utils.test.ts b/src/core/server/saved_objects/utils.test.ts index 64bdf1771decc..033aeea7c018d 100644 --- a/src/core/server/saved_objects/utils.test.ts +++ b/src/core/server/saved_objects/utils.test.ts @@ -421,14 +421,6 @@ describe('convertTypesToLegacySchema', () => { namespaceType: 'multiple', mappings: { properties: {} }, }, - // deprecated test case - { - name: 'typeD', - hidden: false, - namespaceAgnostic: true, - namespaceType: 'multiple', // if namespaceAgnostic and namespaceType are both set, namespaceAgnostic takes precedence - mappings: { properties: {} }, - }, ]; expect(convertTypesToLegacySchema(types)).toEqual({ typeA: { @@ -448,12 +440,6 @@ describe('convertTypesToLegacySchema', () => { isNamespaceAgnostic: false, multiNamespace: true, }, - // deprecated test case - typeD: { - hidden: false, - isNamespaceAgnostic: true, - multiNamespace: false, - }, }); }); }); diff --git a/src/core/server/saved_objects/utils.ts b/src/core/server/saved_objects/utils.ts index 5348963812629..af7c08d1fbfcc 100644 --- a/src/core/server/saved_objects/utils.ts +++ b/src/core/server/saved_objects/utils.ts @@ -82,8 +82,8 @@ export const convertTypesToLegacySchema = ( return { ...schema, [type.name]: { - isNamespaceAgnostic: type.namespaceAgnostic || type.namespaceType === 'agnostic', - multiNamespace: !type.namespaceAgnostic && type.namespaceType === 'multiple', + isNamespaceAgnostic: type.namespaceType === 'agnostic', + multiNamespace: type.namespaceType === 'multiple', hidden: type.hidden, indexPattern: type.indexPattern, convertToAliasScript: type.convertToAliasScript, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 37051da4b17da..7ca5c75f19e8f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -632,7 +632,9 @@ export interface CoreSetup; // (undocumented) - http: HttpServiceSetup; + http: HttpServiceSetup & { + resources: HttpResources; + }; // (undocumented) metrics: MetricsServiceSetup; // (undocumented) @@ -861,6 +863,30 @@ export type Headers = { [header: string]: string | string[] | undefined; }; +// @public +export interface HttpResources { + register: (route: RouteConfig, handler: HttpResourcesRequestHandler) => void; +} + +// @public +export interface HttpResourcesRenderOptions { + headers?: ResponseHeaders; +} + +// @public +export type HttpResourcesRequestHandler

= RequestHandler; + +// @public +export type HttpResourcesResponseOptions = HttpResponseOptions; + +// @public +export interface HttpResourcesServiceToolkit { + renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise; + renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise; + renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse; + renderJs: (options: HttpResourcesResponseOptions) => IKibanaResponse; +} + // @public export interface HttpResponseOptions { body?: HttpResponsePayload; @@ -989,7 +1015,7 @@ export interface IRouter { // // @internal getRoutes: () => RouterRoute[]; - handleLegacyErrors: (handler: RequestHandler) => RequestHandler; + handleLegacyErrors: RequestHandlerWrapper; patch: RouteRegistrar<'patch'>; post: RouteRegistrar<'post'>; put: RouteRegistrar<'put'>; @@ -1008,11 +1034,6 @@ export type ISavedObjectTypeRegistry = Omit; -// @public (undocumented) -export interface IScopedRenderingClient { - render(options?: Pick): Promise; -} - // @public export interface IUiSettingsClient { get: (key: string) => Promise; @@ -1150,6 +1171,10 @@ export interface LegacyServiceSetupDeps { core: LegacyCoreSetup; // (undocumented) plugins: Record; + // Warning: (ae-forgotten-export) The symbol "UiPlugins" needs to be exported by the entry point index.d.ts + // + // (undocumented) + uiPlugins: UiPlugins; } // @public @deprecated (undocumented) @@ -1466,12 +1491,6 @@ export type PluginOpaqueId = symbol; export interface PluginsServiceSetup { contracts: Map; initialized: boolean; - // (undocumented) - uiPlugins: { - internal: Map; - public: Map; - browserConfigs: Map>; - }; } // @internal (undocumented) @@ -1496,19 +1515,13 @@ export type RedirectResponseOptions = HttpResponseOptions & { }; }; -// @internal (undocumented) -export interface RenderingServiceSetup { - render(request: R, uiSettings: IUiSettingsClient, options?: IRenderOptions): Promise; -} - // @public -export type RequestHandler

= (context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) => IKibanaResponse | Promise>; +export type RequestHandler

= (context: RequestHandlerContext, request: KibanaRequest, response: ResponseFactory) => IKibanaResponse | Promise>; // @public export interface RequestHandlerContext { // (undocumented) core: { - rendering: IScopedRenderingClient; savedObjects: { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; @@ -1529,6 +1542,9 @@ export type RequestHandlerContextContainer = IContextContainer = IContextProvider, TContextName>; +// @public +export type RequestHandlerWrapper = (handler: RequestHandler) => RequestHandler; + // @public export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise; @@ -1542,11 +1558,7 @@ export type ResponseError = string | Error | { export type ResponseErrorAttributes = Record; // @public -export type ResponseHeaders = { - [header in KnownHeaders]?: string | string[]; -} & { - [header: string]: string | string[]; -}; +export type ResponseHeaders = Record | Record; // @public export interface RouteConfig { @@ -2251,9 +2263,7 @@ export interface SavedObjectsType { mappings: SavedObjectsTypeMappingDefinition; migrations?: SavedObjectMigrationMap; name: string; - // @deprecated - namespaceAgnostic?: boolean; - namespaceType?: SavedObjectsNamespaceType; + namespaceType: SavedObjectsNamespaceType; } // @public @@ -2458,12 +2468,11 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:162:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:163:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:164:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:166:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugins_service.ts:47:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:163:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:164:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:166:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:167:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:230:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:230:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:232:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 24c41d511180a..1e3e1638cf2a0 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -46,7 +46,10 @@ const rawConfigService = rawConfigServiceMock.create({}); beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - mockPluginsService.discover.mockResolvedValue(new Map()); + mockPluginsService.discover.mockResolvedValue({ + pluginTree: new Map(), + uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, + }); }); afterEach(() => { @@ -88,7 +91,10 @@ test('injects legacy dependency to context#setup()', async () => { [pluginA, []], [pluginB, [pluginA]], ]); - mockPluginsService.discover.mockResolvedValue(pluginDependencies); + mockPluginsService.discover.mockResolvedValue({ + pluginTree: pluginDependencies, + uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, + }); await server.setup(); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 684f50a5666e1..d4c0ebcfb7cf2 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -29,7 +29,8 @@ import { import { CoreApp } from './core_app'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; -import { RenderingService, RenderingServiceSetup } from './rendering'; +import { HttpResourcesService } from './http_resources'; +import { RenderingService } from './rendering'; import { LegacyService, ensureValidConfiguration } from './legacy'; import { Logger, LoggerFactory } from './logging'; import { UiSettingsService } from './ui_settings'; @@ -71,6 +72,7 @@ export class Server { private readonly uiSettings: UiSettingsService; private readonly uuid: UuidService; private readonly metrics: MetricsService; + private readonly httpResources: HttpResourcesService; private readonly status: StatusService; private readonly coreApp: CoreApp; @@ -99,13 +101,14 @@ export class Server { this.metrics = new MetricsService(core); this.status = new StatusService(core); this.coreApp = new CoreApp(core); + this.httpResources = new HttpResourcesService(core); } public async setup() { this.log.debug('setting up server'); // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. - const pluginDependencies = await this.plugins.discover(); + const { pluginTree, uiPlugins } = await this.plugins.discover(); const legacyPlugins = await this.legacy.discoverPlugins(); // Immediately terminate in case of invalid configuration @@ -117,10 +120,7 @@ export class Server { // 1) Can access context from any NP plugin // 2) Can register context providers that will only be available to other legacy plugins and will not leak into // New Platform plugins. - pluginDependencies: new Map([ - ...pluginDependencies, - [this.legacy.legacyId, [...pluginDependencies.keys()]], - ]), + pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]), }); const uuidSetup = await this.uuid.setup(); @@ -148,6 +148,17 @@ export class Server { const metricsSetup = await this.metrics.setup({ http: httpSetup }); + const renderingSetup = await this.rendering.setup({ + http: httpSetup, + legacyPlugins, + uiPlugins, + }); + + const httpResourcesSetup = this.httpResources.setup({ + http: httpSetup, + rendering: renderingSetup, + }); + const statusSetup = this.status.setup({ elasticsearch: elasticsearchServiceSetup, savedObjects: savedObjectsSetup, @@ -158,28 +169,25 @@ export class Server { context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, - metrics: metricsSetup, savedObjects: savedObjectsSetup, status: statusSetup, uiSettings: uiSettingsSetup, uuid: uuidSetup, + metrics: metricsSetup, + rendering: renderingSetup, + httpResources: httpResourcesSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); this.pluginsInitialized = pluginsSetup.initialized; - const renderingSetup = await this.rendering.setup({ - http: httpSetup, - legacyPlugins, - plugins: pluginsSetup, - }); - await this.legacy.setup({ core: { ...coreSetup, plugins: pluginsSetup, rendering: renderingSetup }, plugins: mapToObject(pluginsSetup.contracts), + uiPlugins, }); - this.registerCoreContext(coreSetup, renderingSetup); + this.registerCoreContext(coreSetup); this.coreApp.setup(coreSetup); return coreSetup; @@ -212,7 +220,9 @@ export class Server { }); await this.http.start(); - await this.rendering.start(); + await this.rendering.start({ + legacy: this.legacy, + }); await this.metrics.start(); return this.coreStart; @@ -232,7 +242,7 @@ export class Server { await this.status.stop(); } - private registerCoreContext(coreSetup: InternalCoreSetup, rendering: RenderingServiceSetup) { + private registerCoreContext(coreSetup: InternalCoreSetup) { coreSetup.http.registerRouteHandlerContext( coreId, 'core', @@ -241,13 +251,6 @@ export class Server { const uiSettingsClient = coreSetup.uiSettings.asScopedToClient(savedObjectsClient); return { - rendering: { - render: async (options = {}) => - rendering.render(req, uiSettingsClient, { - ...options, - vars: await this.legacy.legacyInternals!.getVars('core', req), - }), - }, savedObjects: { client: savedObjectsClient, typeRegistry: this.coreStart!.savedObjects.getTypeRegistry(), diff --git a/src/core/server/ui_settings/saved_objects/ui_settings.ts b/src/core/server/ui_settings/saved_objects/ui_settings.ts index 031315bec0dab..1bea65ddee924 100644 --- a/src/core/server/ui_settings/saved_objects/ui_settings.ts +++ b/src/core/server/ui_settings/saved_objects/ui_settings.ts @@ -22,7 +22,7 @@ import { SavedObjectsType } from '../../saved_objects'; export const uiSettingsType: SavedObjectsType = { name: 'config', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { // we don't want to allow `true` in the public `SavedObjectsTypeMappingDefinition` type, however // this is needed for the config that is kinda a special type. To avoid adding additional internal types diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 1d643418997f5..989583742acd0 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -54,11 +54,7 @@ export default function(kibana) { }, uiExports: { - hacks: [ - 'plugins/kibana/discover/legacy', - 'plugins/kibana/dev_tools', - 'plugins/kibana/visualize/legacy', - ], + hacks: ['plugins/kibana/discover/legacy', 'plugins/kibana/dev_tools'], app: { id: 'kibana', title: 'Kibana', diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss index fbfb0a06fabcf..d49c59970f521 100644 --- a/src/legacy/core_plugins/kibana/public/index.scss +++ b/src/legacy/core_plugins/kibana/public/index.scss @@ -13,8 +13,10 @@ // Discover styles @import 'discover/index'; -// Visualize styles -@import './visualize/index'; +// Visualization styles are imported here for running karma Browser tests +// should be somehow included through the "visualizations" plugin initialization +@import '../../../../plugins/visualizations/public/index'; + // Has to come after visualize because of some // bad cascading in the Editor layout @import '../../../../plugins/maps_legacy/public/index'; diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 0a026a5e0c310..20c46765dcb30 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -43,7 +43,6 @@ import 'uiExports/interpreter'; import 'ui/autoload/all'; import './discover/legacy'; -import './visualize/legacy'; import './management'; import './dev_tools'; import { showAppRedirectNotification } from '../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.js.snap deleted file mode 100644 index 59b275c7708a4..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.js.snap +++ /dev/null @@ -1,205 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CreateIndexPatternWizard defaults to the loading state 1`] = ` - -

-
- -
- - -`; - -exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = ` - -
-
- -
- -
-`; - -exports[`CreateIndexPatternWizard renders the empty state when there are no indices 1`] = ` - -
-
- -
- -
-`; - -exports[`CreateIndexPatternWizard renders time field step when step is set to 2 1`] = ` - -
-
- -
- -
-`; - -exports[`CreateIndexPatternWizard renders when there are no indices but there are remote clusters 1`] = ` - -
-
- -
- -
-`; - -exports[`CreateIndexPatternWizard shows system indices even if there are no other indices if the include system indices is toggled 1`] = ` - -
-
- -
- -
-`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap new file mode 100644 index 0000000000000..09a06bd8827ce --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -0,0 +1,312 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CreateIndexPatternWizard defaults to the loading state 1`] = ` + +
+
+ +
+ +
+`; + +exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = ` + +
+
+ +
+ +
+`; + +exports[`CreateIndexPatternWizard renders the empty state when there are no indices 1`] = ` + +
+
+ +
+ +
+`; + +exports[`CreateIndexPatternWizard renders time field step when step is set to 2 1`] = ` + +
+
+ +
+ +
+`; + +exports[`CreateIndexPatternWizard renders when there are no indices but there are remote clusters 1`] = ` + +
+
+ +
+ +
+`; + +exports[`CreateIndexPatternWizard shows system indices even if there are no other indices if the include system indices is toggled 1`] = ` + +
+
+ +
+ +
+`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index 648bf7f8f9738..d8f677b7f6089 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -48,7 +48,7 @@ interface StepIndexPatternProps { esService: DataPublicPluginStart['search']['__LEGACY']['esClient']; savedObjectsClient: SavedObjectsClient; indexPatternCreationType: IndexPatternCreationConfig; - goToNextStep: () => void; + goToNextStep: (query: string) => void; initialQuery?: string; uiSettings: IUiSettingsClient; } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx similarity index 55% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx index 941f87d4d9fd2..45af98661eda3 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx @@ -21,17 +21,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import { CreateIndexPatternWizard } from './create_index_pattern_wizard'; -const mockIndexPatternCreationType = { - getIndexPatternType: () => 'default', - getIndexPatternName: () => 'name', - getIsBeta: () => false, - checkIndicesForErrors: () => false, - getShowSystemIndices: () => false, - renderPrompt: () => {}, - getIndexPatternMappings: () => { - return {}; - }, -}; +import { coreMock } from '../../../../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../../../../../plugins/data/public/mocks'; +import { IndexPatternCreationConfig } from '../../../../../../../../plugins/index_pattern_management/public'; +import { IndexPattern } from '../../../../../../../../plugins/data/public'; +import { SavedObjectsClient } from '../../../../../../../../core/public'; + jest.mock('./components/step_index_pattern', () => ({ StepIndexPattern: 'StepIndexPattern' })); jest.mock('./components/step_time_field', () => ({ StepTimeField: 'StepTimeField' })); jest.mock('./components/header', () => ({ Header: 'Header' })); @@ -46,39 +41,36 @@ jest.mock('ui/chrome', () => ({ addBasePath: () => {}, })); -const loadingDataDocUrl = ''; +const { savedObjects, overlays, uiSettings } = coreMock.createStart(); +const { indexPatterns, search } = dataPluginMock.createStartContract(); +const mockIndexPatternCreationType = new IndexPatternCreationConfig({ + type: 'default', + name: 'name', +}); + const initialQuery = ''; const services = { - es: {}, - indexPatterns: {}, - savedObjectsClient: {}, - config: {}, - changeUrl: () => {}, - scopeApply: () => {}, - + es: search.__LEGACY.esClient, + indexPatterns, + savedObjectsClient: savedObjects.client as SavedObjectsClient, + uiSettings, + changeUrl: jest.fn(), + openConfirm: overlays.openConfirm, indexPatternCreationType: mockIndexPatternCreationType, }; describe('CreateIndexPatternWizard', () => { - it(`defaults to the loading state`, async () => { + test(`defaults to the loading state`, () => { const component = shallow( - + ); expect(component).toMatchSnapshot(); }); - it('renders the empty state when there are no indices', async () => { + test('renders the empty state when there are no indices', async () => { const component = shallow( - + ); component.setState({ @@ -91,13 +83,9 @@ describe('CreateIndexPatternWizard', () => { expect(component).toMatchSnapshot(); }); - it('renders when there are no indices but there are remote clusters', async () => { + test('renders when there are no indices but there are remote clusters', async () => { const component = shallow( - + ); component.setState({ @@ -110,13 +98,9 @@ describe('CreateIndexPatternWizard', () => { expect(component).toMatchSnapshot(); }); - it('shows system indices even if there are no other indices if the include system indices is toggled', async () => { + test('shows system indices even if there are no other indices if the include system indices is toggled', async () => { const component = shallow( - + ); component.setState({ @@ -129,13 +113,9 @@ describe('CreateIndexPatternWizard', () => { expect(component).toMatchSnapshot(); }); - it('renders index pattern step when there are indices', async () => { + test('renders index pattern step when there are indices', async () => { const component = shallow( - + ); component.setState({ @@ -147,13 +127,9 @@ describe('CreateIndexPatternWizard', () => { expect(component).toMatchSnapshot(); }); - it('renders time field step when step is set to 2', async () => { + test('renders time field step when step is set to 2', async () => { const component = shallow( - + ); component.setState({ @@ -166,37 +142,30 @@ describe('CreateIndexPatternWizard', () => { expect(component).toMatchSnapshot(); }); - it('invokes the provided services when creating an index pattern', async () => { - const get = jest.fn(); - const set = jest.fn(); + test('invokes the provided services when creating an index pattern', async () => { const create = jest.fn().mockImplementation(() => 'id'); const clear = jest.fn(); - const changeUrl = jest.fn(); - - const component = shallow( - ({ - create, - }), - clearCache: clear, - }, - changeUrl, - indexPatternCreationType: mockIndexPatternCreationType, - }} - /> + services.indexPatterns.clearCache = clear; + const indexPattern = ({ + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + fields: [], + create, + } as unknown) as IndexPattern; + services.indexPatterns.make = async () => { + return indexPattern; + }; + + const component = shallow( + ); component.setState({ indexPattern: 'foo' }); - await component.instance().createIndexPattern(null, 'id'); - expect(get).toBeCalled(); + await component.instance().createIndexPattern(undefined, 'id'); + expect(services.uiSettings.get).toBeCalled(); expect(create).toBeCalled(); expect(clear).toBeCalledWith('id'); - expect(changeUrl).toBeCalledWith(`/management/kibana/index_patterns/id`); + expect(services.changeUrl).toBeCalledWith(`/management/kibana/index_patterns/id`); }); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.tsx similarity index 63% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.tsx index 1a93188edd6cc..4166d48349d35 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.tsx @@ -17,11 +17,11 @@ * under the License. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { ReactElement, Component } from 'react'; -import { EuiGlobalToastList } from '@elastic/eui'; +import { EuiGlobalToastList, EuiGlobalToastListToast } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { StepIndexPattern } from './components/step_index_pattern'; import { StepTimeField } from './components/step_time_field'; @@ -31,41 +31,61 @@ import { EmptyState } from './components/empty_state'; import { MAX_SEARCH_SIZE } from './constants'; import { ensureMinimumTime, getIndices } from './lib'; -import { i18n } from '@kbn/i18n'; - -export class CreateIndexPatternWizard extends Component { - static propTypes = { - initialQuery: PropTypes.string, - services: PropTypes.shape({ - es: PropTypes.object.isRequired, - indexPatterns: PropTypes.object.isRequired, - savedObjectsClient: PropTypes.object.isRequired, - indexPatternCreationType: PropTypes.object.isRequired, - config: PropTypes.object.isRequired, - changeUrl: PropTypes.func.isRequired, - openConfirm: PropTypes.func.isRequired, - }).isRequired, +import { + SavedObjectsClient, + IUiSettingsClient, + OverlayStart, +} from '../../../../../../../../core/public'; +import { DataPublicPluginStart } from '../../../../../../../../plugins/data/public'; +import { IndexPatternCreationConfig } from '../../../../../../../../plugins/index_pattern_management/public'; +import { MatchedIndex } from './types'; + +interface CreateIndexPatternWizardProps { + initialQuery: string; + services: { + indexPatternCreationType: IndexPatternCreationConfig; + es: DataPublicPluginStart['search']['__LEGACY']['esClient']; + indexPatterns: DataPublicPluginStart['indexPatterns']; + savedObjectsClient: SavedObjectsClient; + uiSettings: IUiSettingsClient; + changeUrl: (url: string) => void; + openConfirm: OverlayStart['openConfirm']; }; +} - constructor(props) { - super(props); - this.indexPatternCreationType = this.props.services.indexPatternCreationType; - this.state = { - step: 1, - indexPattern: '', - allIndices: [], - remoteClustersExist: false, - isInitiallyLoadingIndices: true, - isIncludingSystemIndices: false, - toasts: [], - }; - } +interface CreateIndexPatternWizardState { + step: number; + indexPattern: string; + allIndices: MatchedIndex[]; + remoteClustersExist: boolean; + isInitiallyLoadingIndices: boolean; + isIncludingSystemIndices: boolean; + toasts: EuiGlobalToastListToast[]; +} + +export class CreateIndexPatternWizard extends Component< + CreateIndexPatternWizardProps, + CreateIndexPatternWizardState +> { + state = { + step: 1, + indexPattern: '', + allIndices: [], + remoteClustersExist: false, + isInitiallyLoadingIndices: true, + isIncludingSystemIndices: false, + toasts: [], + }; async UNSAFE_componentWillMount() { this.fetchData(); } - catchAndWarn = async (asyncFn, errorValue, errorMsg) => { + catchAndWarn = async ( + asyncFn: Promise, + errorValue: [] | string[], + errorMsg: ReactElement + ) => { try { return await asyncFn; } catch (errors) { @@ -109,22 +129,26 @@ export class CreateIndexPatternWizard extends Component { // query local and remote indices, updating state independently ensureMinimumTime( this.catchAndWarn( - getIndices(services.es, this.indexPatternCreationType, `*`, MAX_SEARCH_SIZE), + getIndices(services.es, services.indexPatternCreationType, `*`, MAX_SEARCH_SIZE), [], indicesFailMsg ) - ).then(allIndices => this.setState({ allIndices, isInitiallyLoadingIndices: false })); + ).then((allIndices: MatchedIndex[]) => + this.setState({ allIndices, isInitiallyLoadingIndices: false }) + ); this.catchAndWarn( // if we get an error from remote cluster query, supply fallback value that allows user entry. // ['a'] is fallback value - getIndices(services.es, this.indexPatternCreationType, `*:*`, 1), + getIndices(services.es, services.indexPatternCreationType, `*:*`, 1), ['a'], clustersFailMsg - ).then(remoteIndices => this.setState({ remoteClustersExist: !!remoteIndices.length })); + ).then((remoteIndices: string[] | MatchedIndex[]) => + this.setState({ remoteClustersExist: !!remoteIndices.length }) + ); }; - createIndexPattern = async (timeFieldName, indexPatternId) => { + createIndexPattern = async (timeFieldName: string | undefined, indexPatternId: string) => { const { services } = this.props; const { indexPattern } = this.state; @@ -134,13 +158,13 @@ export class CreateIndexPatternWizard extends Component { id: indexPatternId, title: indexPattern, timeFieldName, - ...this.indexPatternCreationType.getIndexPatternMappings(), + ...services.indexPatternCreationType.getIndexPatternMappings(), }); const createdId = await emptyPattern.create(); if (!createdId) { const confirmMessage = i18n.translate('kbn.management.indexPattern.titleExistsLabel', { - values: { title: this.title }, + values: { title: emptyPattern.title }, defaultMessage: "An index pattern with the title '{title}' already exists.", }); @@ -157,15 +181,15 @@ export class CreateIndexPatternWizard extends Component { } } - if (!services.config.get('defaultIndex')) { - await services.config.set('defaultIndex', createdId); + if (!services.uiSettings.get('defaultIndex')) { + await services.uiSettings.set('defaultIndex', createdId); } services.indexPatterns.clearCache(createdId); services.changeUrl(`/management/kibana/index_patterns/${createdId}`); }; - goToTimeFieldStep = indexPattern => { + goToTimeFieldStep = (indexPattern: string) => { this.setState({ step: 2, indexPattern }); }; @@ -174,22 +198,23 @@ export class CreateIndexPatternWizard extends Component { }; onChangeIncludingSystemIndices = () => { - this.setState(state => ({ - isIncludingSystemIndices: !state.isIncludingSystemIndices, + this.setState(prevState => ({ + isIncludingSystemIndices: !prevState.isIncludingSystemIndices, })); }; renderHeader() { const { isIncludingSystemIndices } = this.state; + const { services } = this.props; return (
); } @@ -208,7 +233,7 @@ export class CreateIndexPatternWizard extends Component { return ; } - const hasDataIndices = allIndices.some(({ name }) => !name.startsWith('.')); + const hasDataIndices = allIndices.some(({ name }: MatchedIndex) => !name.startsWith('.')); if (!hasDataIndices && !isIncludingSystemIndices && !remoteClustersExist) { return ; } @@ -222,7 +247,7 @@ export class CreateIndexPatternWizard extends Component { isIncludingSystemIndices={isIncludingSystemIndices} esService={services.es} savedObjectsClient={services.savedObjectsClient} - indexPatternCreationType={this.indexPatternCreationType} + indexPatternCreationType={services.indexPatternCreationType} goToNextStep={this.goToTimeFieldStep} uiSettings={services.uiSettings} /> @@ -237,7 +262,7 @@ export class CreateIndexPatternWizard extends Component { indexPatternsService={services.indexPatterns} goToPreviousStep={this.goToIndexPatternStep} createIndexPattern={this.createIndexPattern} - indexPatternCreationType={this.indexPatternCreationType} + indexPatternCreationType={services.indexPatternCreationType} /> ); } @@ -245,9 +270,9 @@ export class CreateIndexPatternWizard extends Component { return null; } - removeToast = removedToast => { + removeToast = (id: string) => { this.setState(prevState => ({ - toasts: prevState.toasts.filter(toast => toast.id !== removedToast.id), + toasts: prevState.toasts.filter(toast => toast.id !== id), })); }; @@ -263,7 +288,9 @@ export class CreateIndexPatternWizard extends Component { { + this.removeToast(id); + }} toastLifeTimeMs={6000} /> diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js index 47cb773258cb4..ed1fc026c560c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js @@ -36,17 +36,15 @@ uiRoutes.when('/management/kibana/index_pattern', { $routeParams.type ); const services = { - config: npStart.core.uiSettings, + uiSettings: npStart.core.uiSettings, es: npStart.plugins.data.search.__LEGACY.esClient, indexPatterns: npStart.plugins.data.indexPatterns, - $http: npStart.core.http, savedObjectsClient: npStart.core.savedObjects.client, indexPatternCreationType, changeUrl: url => { $scope.$evalAsync(() => kbnUrl.changePath(url)); }, openConfirm: npStart.core.overlays.openConfirm, - uiSettings: npStart.core.uiSettings, }; const initialQuery = $routeParams.id ? decodeURIComponent($routeParams.id) : undefined; diff --git a/src/legacy/core_plugins/kibana/public/visualize/_index.scss b/src/legacy/core_plugins/kibana/public/visualize/_index.scss deleted file mode 100644 index 079d82936bb57..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/_index.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Visualize plugin styles -@import 'np_ready/index'; - -// should be removed while moving the visualize into NP -@import '../../../../../plugins/vis_default_editor/public/index' diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts deleted file mode 100644 index f6d73b987912d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ /dev/null @@ -1,36 +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. - */ - -/** - * The imports in this file are static functions and types which still live in legacy folders and are used - * within dashboard. To consolidate them all in one place, they are re-exported from this file. Eventually - * this list should become empty. Imports from the top level of shimmed or moved plugins can be imported - * directly where they are needed. - */ - -export { DashboardConstants } from '../../../../../plugins/dashboard/public'; -export { - VisSavedObject, - VISUALIZE_EMBEDDABLE_TYPE, -} from '../../../../../plugins/visualizations/public/'; -export { - configureAppAngularModule, - migrateLegacyQuery, - subscribeWithScope, -} from '../../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index c15318d29e761..7f5c7d4664af8 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -27,10 +27,11 @@ import { fatalError, toastNotifications } from 'ui/notify'; import { timefilter } from 'ui/timefilter'; import { npStart } from 'ui/new_platform'; import { getSavedSheetBreadcrumbs, getCreateBreadcrumbs } from './breadcrumbs'; -import { getTimezone } from '../../vis_type_timelion/public'; +import { getTimezone } from '../../../../plugins/vis_type_timelion/public'; import 'uiExports/savedObjectTypes'; +require('ui/i18n'); require('ui/autoload/all'); // TODO: remove ui imports completely (move to plugins) @@ -57,7 +58,7 @@ require('plugins/timelion/directives/timelion_options_sheet'); document.title = 'Timelion - Kibana'; -const app = require('ui/modules').get('apps/timelion', []); +const app = require('ui/modules').get('apps/timelion', ['i18n', 'ngSanitize']); require('ui/routes').enable(); diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js index 57262fda55e48..35ac883e5d99c 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js +++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js @@ -43,7 +43,7 @@ import _ from 'lodash'; import $ from 'jquery'; import PEG from 'pegjs'; -import grammar from 'raw-loader!../../../../../plugins/timelion/common/chain.peg'; +import grammar from 'raw-loader!../../../../../plugins/vis_type_timelion/common/chain.peg'; import timelionExpressionInputTemplate from './timelion_expression_input.html'; import { SUGGESTION_TYPE, @@ -52,7 +52,7 @@ import { insertAtLocation, } from './timelion_expression_input_helpers'; import { comboBoxKeyCodes } from '@elastic/eui'; -import { getArgValueSuggestions } from '../../../vis_type_timelion/public/helpers/arg_value_suggestions'; +import { npStart } from 'ui/new_platform'; const Parser = PEG.generate(grammar); @@ -68,7 +68,7 @@ export function TimelionExpInput($http, $timeout) { replace: true, template: timelionExpressionInputTemplate, link: function(scope, elem) { - const argValueSuggestions = getArgValueSuggestions(); + const argValueSuggestions = npStart.plugins.visTypeTimelion.getArgValueSuggestions(); const expressionInput = elem.find('[data-expression-input]'); const functionReference = {}; let suggestibleFunctionLocation = {}; diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts index cd40cbfa89ffe..34b389f5ff4ce 100644 --- a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts +++ b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts @@ -17,7 +17,8 @@ * under the License. */ -import '../../../../vis_type_timelion/public/flot'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../../../plugins/vis_type_timelion/public/flot'; import _ from 'lodash'; import $ from 'jquery'; import moment from 'moment-timezone'; @@ -28,11 +29,14 @@ import { calculateInterval, DEFAULT_TIME_FORMAT, // @ts-ignore -} from '../../../../../../plugins/timelion/common/lib'; -import { tickFormatters } from '../../../../vis_type_timelion/public/helpers/tick_formatters'; +} from '../../../../../../plugins/vis_type_timelion/common/lib'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { tickFormatters } from '../../../../../../plugins/vis_type_timelion/public/helpers/tick_formatters'; import { TimelionVisualizationDependencies } from '../../plugin'; -import { xaxisFormatterProvider } from '../../../../vis_type_timelion/public/helpers/xaxis_formatter'; -import { generateTicksProvider } from '../../../../vis_type_timelion/public/helpers/tick_generator'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { xaxisFormatterProvider } from '../../../../../../plugins/vis_type_timelion/public/helpers/xaxis_formatter'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { generateTicksProvider } from '../../../../../../plugins/vis_type_timelion/public/helpers/tick_generator'; const DEBOUNCE_DELAY = 50; diff --git a/src/legacy/core_plugins/vis_type_markdown/index.ts b/src/legacy/core_plugins/vis_type_markdown/index.ts deleted file mode 100644 index 3c00420e57d55..0000000000000 --- a/src/legacy/core_plugins/vis_type_markdown/index.ts +++ /dev/null @@ -1,44 +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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const markdownPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'markdown_vis', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default markdownPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_markdown/package.json b/src/legacy/core_plugins/vis_type_markdown/package.json deleted file mode 100644 index 5c233d82fe506..0000000000000 --- a/src/legacy/core_plugins/vis_type_markdown/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "markdown_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_markdown/public/legacy.ts b/src/legacy/core_plugins/vis_type_markdown/public/legacy.ts deleted file mode 100644 index 1cfc583f6e005..0000000000000 --- a/src/legacy/core_plugins/vis_type_markdown/public/legacy.ts +++ /dev/null @@ -1,33 +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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { MarkdownPluginSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core); diff --git a/src/legacy/core_plugins/vis_type_timelion/index.ts b/src/legacy/core_plugins/vis_type_timelion/index.ts deleted file mode 100644 index 7bca5154c84fd..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/index.ts +++ /dev/null @@ -1,44 +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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const timelionVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'timelion_vis', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - }); - -// eslint-disable-next-line import/no-default-export -export default timelionVisPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_timelion/package.json b/src/legacy/core_plugins/vis_type_timelion/package.json deleted file mode 100644 index 9b09f98ce6caf..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "timelion_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/_index.scss b/src/legacy/core_plugins/vis_type_timelion/public/components/_index.scss deleted file mode 100644 index 1d887f43ff9a1..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './panel'; -@import './timelion_expression_input'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/legacy.ts b/src/legacy/core_plugins/vis_type_timelion/public/legacy.ts deleted file mode 100644 index f8de9f94dcedf..0000000000000 --- a/src/legacy/core_plugins/vis_type_timelion/public/legacy.ts +++ /dev/null @@ -1,35 +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 { PluginInitializerContext } from 'kibana/public'; - -import { npSetup, npStart } from './legacy_imports'; -import { TimelionVisSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, - data: npSetup.plugins.data, - visualizations: npSetup.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index a9b8c29374854..0d2f3528c9019 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -41,10 +41,11 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LegacyConfig, ILegacyService, ILegacyInternals } from '../../core/server/legacy'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { UiPlugins } from '../../core/server/plugins'; import { ApmOssPlugin } from '../core_plugins/apm_oss'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; import { UsageCollectionSetup } from '../../plugins/usage_collection/server'; -import { Capabilities } from '../../core/server'; import { UiSettingsServiceFactoryOptions } from '../../legacy/ui/ui_settings/ui_settings_service_factory'; import { HomeServerPluginSetup } from '../../plugins/home/server'; @@ -111,7 +112,7 @@ export interface KibanaCore { kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator']; legacy: ILegacyInternals; rendering: LegacyServiceSetupDeps['core']['rendering']; - uiPlugins: LegacyServiceSetupDeps['core']['plugins']['uiPlugins']; + uiPlugins: UiPlugins; uiSettings: LegacyServiceSetupDeps['core']['uiSettings']; savedObjectsClientProvider: LegacyServiceStartDeps['core']['savedObjects']['clientProvider']; }; diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index 87006d9347de4..aaed52f8b120a 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -17,9 +17,3 @@ @import './field_editor/index'; @import './style_compile/index'; @import '../../../plugins/management/public/components/index'; - -// The following are prefixed with "vis" - -// Can't import vis folder here because of cascading issues, it's imported in core_plugins/kibana -// @import './vis/index'; -@import './visualize/index'; diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 80fb837258d4c..5ae2e2348aaa1 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -69,6 +69,7 @@ import { VisualizationsSetup, VisualizationsStart, } from '../../../../plugins/visualizations/public'; +import { VisTypeTimelionPluginStart } from '../../../../plugins/vis_type_timelion/public'; import { MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; export interface PluginsSetup { @@ -116,6 +117,7 @@ export interface PluginsStart { telemetry?: TelemetryPluginStart; dashboard: DashboardStart; savedObjectsManagement: SavedObjectsManagementPluginStart; + visTypeTimelion: VisTypeTimelionPluginStart; indexPatternManagement: IndexPatternManagementStart; } diff --git a/src/legacy/ui/public/visualize/_index.scss b/src/legacy/ui/public/visualize/_index.scss deleted file mode 100644 index d9761f741353b..0000000000000 --- a/src/legacy/ui/public/visualize/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import '../../../../plugins/visualizations/public/components/index'; diff --git a/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts b/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts new file mode 100644 index 0000000000000..b226bad11a01a --- /dev/null +++ b/src/plugins/console/server/__tests__/proxy_route/proxy_fallback.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { duration } from 'moment'; +import { getProxyRouteHandlerDeps } from './mocks'; + +import { kibanaResponseFactory } from '../../../../../core/server'; +import { createHandler } from '../../routes/api/console/proxy/create_handler'; +import * as requestModule from '../../lib/proxy_request'; + +describe('Console Proxy Route', () => { + afterEach(async () => { + jest.resetAllMocks(); + }); + + describe('fallback behaviour', () => { + it('falls back to all configured endpoints regardless of error', async () => { + // Describe a situation where all three configured nodes reject + (requestModule.proxyRequest as jest.Mock).mockRejectedValueOnce(new Error('ECONNREFUSED')); + (requestModule.proxyRequest as jest.Mock).mockRejectedValueOnce(new Error('EHOSTUNREACH')); + (requestModule.proxyRequest as jest.Mock).mockRejectedValueOnce(new Error('ESOCKETTIMEDOUT')); + + const handler = createHandler( + getProxyRouteHandlerDeps({ + readLegacyESConfig: () => ({ + requestTimeout: duration(30000), + customHeaders: {}, + requestHeadersWhitelist: [], + hosts: ['http://localhost:9201', 'http://localhost:9202', 'http://localhost:9203'], + }), + }) + ); + + const response = await handler( + {} as any, + { + headers: {}, + query: { method: 'get', path: 'test' }, + } as any, + kibanaResponseFactory + ); + + expect(response.status).toBe(502); + // Return the message from the ES node we attempted last. + expect(response.payload.message).toBe('ESOCKETTIMEDOUT'); + }); + }); +}); diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json index 408b01c4cb8f5..0da2c130b47cf 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_settings.json @@ -59,7 +59,61 @@ } }, "transient": { - "__scope_link": ".persistent" + "cluster": { + "routing": { + "allocation.enable": { + "__one_of": ["all", "primaries", "new_primaries", "none"] + }, + "allocation.disk.threshold_enabled": { "__one_of": [false, true] }, + "allocation.disk.watermark.low": "85%", + "allocation.disk.watermark.high": "90%", + "allocation.disk.reroute_interval": "60s", + "allocation.exclude": { + "_ip": "", + "_name": "", + "_host": "", + "_id": "" + }, + "allocation.include": { + "_ip": "", + "_name": "", + "_host": "", + "_id": "" + }, + "allocation.require": { + "_ip": "", + "_name": "", + "_host": "", + "_id": "" + }, + "allocation.awareness.attributes": [], + "allocation.awareness.force": { + "*": { + "values": [] + } + }, + "allocation.allow_rebalance": { + "__one_of": [ + "always", + "indices_primaries_active", + "indices_all_active" + ] + }, + "allocation.cluster_concurrent_rebalance": 2, + "allocation.node_initial_primaries_recoveries": 4, + "allocation.node_concurrent_recoveries": 2, + "allocation.same_shard.host": { "__one_of": [false, true] } + } + }, + "indices": { + "breaker": { + "total.limit": "70%", + "fielddata.limit": "60%", + "fielddata.overhead": 1.03, + "request.limit": "40%", + "request.overhead": 1.0 + } + } } } } diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts index 50a9fcf03c209..9446289ff03ea 100644 --- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts +++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts @@ -175,10 +175,9 @@ export const createHandler = ({ break; } catch (e) { + // If we reached here it means we hit a lower level network issue than just, for e.g., a 500. + // We try contacting another node in that case. log.error(e); - if (e.code !== 'ECONNREFUSED') { - return response.internalError(e); - } if (idx === hosts.length - 1) { log.warn(`Could not connect to any configured ES node [${hosts.join(', ')}]`); return response.customError({ diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data/server/saved_objects/index_patterns.ts index 9838071eee5a4..497dbb7d6f630 100644 --- a/src/plugins/data/server/saved_objects/index_patterns.ts +++ b/src/plugins/data/server/saved_objects/index_patterns.ts @@ -23,7 +23,7 @@ import { indexPatternSavedObjectTypeMigrations } from './index_pattern_migration export const indexPatternSavedObjectType: SavedObjectsType = { name: 'index-pattern', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', management: { icon: 'indexPatternApp', defaultSearchField: 'title', diff --git a/src/plugins/data/server/saved_objects/query.ts b/src/plugins/data/server/saved_objects/query.ts index ff0a6cfde8113..015cc7c6cb134 100644 --- a/src/plugins/data/server/saved_objects/query.ts +++ b/src/plugins/data/server/saved_objects/query.ts @@ -22,7 +22,7 @@ import { SavedObjectsType } from 'kibana/server'; export const querySavedObjectType: SavedObjectsType = { name: 'query', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', management: { icon: 'search', defaultSearchField: 'title', diff --git a/src/plugins/data/server/saved_objects/search.ts b/src/plugins/data/server/saved_objects/search.ts index 8b30ff7d08201..54de35a90ae02 100644 --- a/src/plugins/data/server/saved_objects/search.ts +++ b/src/plugins/data/server/saved_objects/search.ts @@ -23,7 +23,7 @@ import { searchSavedObjectTypeMigrations } from './search_migrations'; export const searchSavedObjectType: SavedObjectsType = { name: 'search', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', management: { icon: 'discoverApp', defaultSearchField: 'title', diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index 9ad9f14ac5659..9bec91b859ab7 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -25,7 +25,7 @@ export * from './ui_settings'; export * from './field_icon'; export * from './table_list_view'; export * from './split_panel'; -export { ValidatedDualRange } from './validated_range'; +export { ValidatedDualRange, Value } from './validated_range'; export * from './notifications'; export { Markdown, MarkdownSimple } from './markdown'; export { reactToUiComponent, uiToReactComponent } from './adapters'; diff --git a/src/plugins/kibana_react/public/validated_range/index.ts b/src/plugins/kibana_react/public/validated_range/index.ts index bc643373f5e60..7d720ec842a43 100644 --- a/src/plugins/kibana_react/public/validated_range/index.ts +++ b/src/plugins/kibana_react/public/validated_range/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { ValidatedDualRange } from './validated_dual_range'; +export { ValidatedDualRange, Value } from './validated_dual_range'; diff --git a/src/plugins/kibana_utils/public/core/create_start_service_getter.test.ts b/src/plugins/kibana_utils/public/core/create_start_service_getter.test.ts new file mode 100644 index 0000000000000..9d9b21269e102 --- /dev/null +++ b/src/plugins/kibana_utils/public/core/create_start_service_getter.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { StartServicesAccessor } from '../../../../core/public'; +import { createStartServicesGetter } from './create_start_service_getter'; +import { Defer } from '../../common/defer'; + +describe('createStartServicesGetter', () => { + test('throws if services are accessed before accessor resolves', async () => { + const future = new Defer(); + const accessor: StartServicesAccessor = async () => await future.promise; + const start = createStartServicesGetter(accessor); + + await new Promise(r => setTimeout(r, 1)); + + expect(() => start()).toThrowErrorMatchingInlineSnapshot( + `"Trying to access start services before start."` + ); + }); + + test('returns services after accessor resolves even if first time called before it resolved', async () => { + const future = new Defer(); + const core = {}; + const plugins = {}; + const self = {}; + const accessor: StartServicesAccessor = async () => await future.promise; + const start = createStartServicesGetter(accessor); + + await new Promise(r => setTimeout(r, 1)); + + expect(() => start()).toThrow(); + + await new Promise(r => setTimeout(r, 1)); + future.resolve([core, plugins, self]); + await future.promise; + + expect(start()).toEqual({ + core, + plugins, + self, + }); + }); + + test('returns services if called after accessor resolves', async () => { + const future = new Defer(); + const core = {}; + const plugins = {}; + const self = {}; + const accessor: StartServicesAccessor = async () => await future.promise; + const start = createStartServicesGetter(accessor); + + await new Promise(r => setTimeout(r, 1)); + future.resolve([core, plugins, self]); + await future.promise; + + expect(start()).toEqual({ + core, + plugins, + self, + }); + }); +}); diff --git a/src/plugins/kibana_utils/public/core/create_start_service_getter.ts b/src/plugins/kibana_utils/public/core/create_start_service_getter.ts new file mode 100644 index 0000000000000..e507d1ae778e5 --- /dev/null +++ b/src/plugins/kibana_utils/public/core/create_start_service_getter.ts @@ -0,0 +1,56 @@ +/* + * 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 { CoreStart, StartServicesAccessor } from '../../../../core/public'; + +export interface StartServices { + plugins: Plugins; + self: OwnContract; + core: CoreStart; +} + +export type StartServicesGetter = () => StartServices< + Plugins, + OwnContract +>; + +export const createStartServicesGetter = ( + accessor: StartServicesAccessor +): StartServicesGetter => { + let services: StartServices | undefined; + + accessor().then( + ([core, plugins, self]) => { + services = { + core, + plugins, + self, + }; + }, + error => { + // eslint-disable-next-line no-console + console.error('Could not access start services.', error); + } + ); + + return () => { + if (!services) throw new Error('Trying to access start services before start.'); + return services; + }; +}; diff --git a/src/plugins/kibana_utils/public/core/index.ts b/src/plugins/kibana_utils/public/core/index.ts index 3f08d591300a2..8bbb2129071f5 100644 --- a/src/plugins/kibana_utils/public/core/index.ts +++ b/src/plugins/kibana_utils/public/core/index.ts @@ -18,3 +18,4 @@ */ export * from './create_kibana_utils_core'; +export * from './create_start_service_getter'; diff --git a/src/plugins/saved_objects_management/server/services/management.test.ts b/src/plugins/saved_objects_management/server/services/management.test.ts index 6b95048749fae..3625a3f913444 100644 --- a/src/plugins/saved_objects_management/server/services/management.test.ts +++ b/src/plugins/saved_objects_management/server/services/management.test.ts @@ -28,7 +28,7 @@ describe('SavedObjectsManagement', () => { registry.registerType({ name: 'unknown', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: {} }, migrations: {}, ...type, diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts index 0c5b74915e58a..747af3b9e57df 100644 --- a/src/plugins/share/server/routes/goto.ts +++ b/src/plugins/share/server/routes/goto.ts @@ -34,7 +34,7 @@ export const createGotoRoute = ({ shortUrlLookup: ShortUrlLookupService; http: CoreSetup['http']; }) => { - router.get( + http.resources.register( { path: getGotoPath('{urlId}'), validate: { @@ -63,14 +63,8 @@ export const createGotoRoute = ({ }, }); } - const body = await context.core.rendering.render(); - return response.ok({ - headers: { - 'content-security-policy': http.csp.header, - }, - body, - }); + return response.renderCoreApp(); }) ); }; diff --git a/src/plugins/telemetry/server/collectors/application_usage/saved_objects_types.ts b/src/plugins/telemetry/server/collectors/application_usage/saved_objects_types.ts index 9f997ab7b5df3..a0de79da565e6 100644 --- a/src/plugins/telemetry/server/collectors/application_usage/saved_objects_types.ts +++ b/src/plugins/telemetry/server/collectors/application_usage/saved_objects_types.ts @@ -33,7 +33,7 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe registerType({ name: 'application_usage_totals', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { appId: { type: 'keyword' }, @@ -46,7 +46,7 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe registerType({ name: 'application_usage_transactional', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { timestamp: { type: 'date' }, diff --git a/src/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts index 3f6e1836cac7d..603742f612a6b 100644 --- a/src/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts +++ b/src/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -38,7 +38,7 @@ export function registerUiMetricUsageCollector( registerType({ name: 'ui-metric', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { count: { diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 1df6a665e4d76..d1530c272027a 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -125,7 +125,7 @@ export class TelemetryPlugin implements Plugin { registerType({ name: 'telemetry', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { enabled: { diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json deleted file mode 100644 index dddfd6c67e655..0000000000000 --- a/src/plugins/timelion/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "timelion", - "version": "8.0.0", - "kibanaVersion": "kibana", - "configPath": ["timelion"], - "server": true, - "ui": true -} diff --git a/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts index c2ac63c98cbea..dac86249ebbb9 100644 --- a/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts +++ b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts @@ -92,7 +92,7 @@ function validateValueUnique( isDuplicate: false, }; - if (inputValue && list.indexOf(inputValue) !== index) { + if (inputValue !== EMPTY_STRING && list.indexOf(inputValue) !== index) { result.isDuplicate = true; result.error = i18n.translate( 'visDefaultEditor.controls.numberList.duplicateValueErrorMessage', diff --git a/src/plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx index f1963b94dcf95..1c2ddbc314f99 100644 --- a/src/plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -19,7 +19,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; +import { EditorRenderProps } from 'src/plugins/visualize/public'; import { PanelsContainer, Panel } from '../../kibana_react/public'; import './vis_type_agg_filter'; diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index 798da09f8e30b..014c69f50d558 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -22,7 +22,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { EventEmitter } from 'events'; -import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; +import { EditorRenderProps } from 'src/plugins/visualize/public'; import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; import { Storage } from '../../kibana_utils/public'; import { KibanaContextProvider } from '../../kibana_react/public'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/legacy_imports.ts b/src/plugins/vis_type_markdown/config.ts similarity index 79% rename from src/legacy/core_plugins/vis_type_timelion/public/legacy_imports.ts rename to src/plugins/vis_type_markdown/config.ts index e7612b288fb24..6749bd83de39f 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/legacy_imports.ts +++ b/src/plugins/vis_type_markdown/config.ts @@ -17,5 +17,10 @@ * under the License. */ -export { npSetup, npStart } from 'ui/new_platform'; -export { PluginsStart } from 'ui/new_platform/new_platform'; +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_markdown/kibana.json b/src/plugins/vis_type_markdown/kibana.json new file mode 100644 index 0000000000000..d52e22118ccf0 --- /dev/null +++ b/src/plugins/vis_type_markdown/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "visTypeMarkdown", + "version": "kibana", + "ui": true, + "server": true, + "requiredPlugins": ["expressions", "visualizations"] +} diff --git a/src/legacy/core_plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap b/src/plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap rename to src/plugins/vis_type_markdown/public/__snapshots__/markdown_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_markdown/public/_markdown_vis.scss b/src/plugins/vis_type_markdown/public/_markdown_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_markdown/public/_markdown_vis.scss rename to src/plugins/vis_type_markdown/public/_markdown_vis.scss diff --git a/src/legacy/core_plugins/vis_type_markdown/public/index.scss b/src/plugins/vis_type_markdown/public/index.scss similarity index 76% rename from src/legacy/core_plugins/vis_type_markdown/public/index.scss rename to src/plugins/vis_type_markdown/public/index.scss index ebae2d1936a9e..ddb7fe3a6b0d9 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/index.scss +++ b/src/plugins/vis_type_markdown/public/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Prefix all styles with "mkd" to avoid conflicts. // Examples // mkdChart diff --git a/src/legacy/core_plugins/vis_type_markdown/public/index.ts b/src/plugins/vis_type_markdown/public/index.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_markdown/public/index.ts rename to src/plugins/vis_type_markdown/public/index.ts index 22dd61b6bda9c..bc3b6ff7bf105 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/index.ts +++ b/src/plugins/vis_type_markdown/public/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../core/public'; +import { PluginInitializerContext } from '../../../core/public'; import { MarkdownPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.test.ts b/src/plugins/vis_type_markdown/public/markdown_fn.test.ts similarity index 89% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.test.ts rename to src/plugins/vis_type_markdown/public/markdown_fn.test.ts index 5f41840bac99b..d6085804e74ab 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.test.ts +++ b/src/plugins/vis_type_markdown/public/markdown_fn.test.ts @@ -17,8 +17,7 @@ * under the License. */ -// eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; import { createMarkdownVisFn } from './markdown_fn'; describe('interpreter/functions#markdown', () => { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.ts b/src/plugins/vis_type_markdown/public/markdown_fn.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.ts rename to src/plugins/vis_type_markdown/public/markdown_fn.ts index bbf2b7844c73f..9f0809109e465 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_fn.ts +++ b/src/plugins/vis_type_markdown/public/markdown_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, Render } from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, Render } from '../../expressions/public'; import { Arguments, MarkdownVisParams } from './types'; interface RenderValue { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx b/src/plugins/vis_type_markdown/public/markdown_options.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_options.tsx rename to src/plugins/vis_type_markdown/public/markdown_options.tsx diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts similarity index 96% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts rename to src/plugins/vis_type_markdown/public/markdown_vis.ts index 57ea6d9c9bb3d..b84d9638eb973 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { MarkdownVisWrapper } from './markdown_vis_controller'; import { MarkdownOptions } from './markdown_options'; import { SettingsOptions } from './settings_options'; -import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; +import { DefaultEditorSize } from '../../vis_default_editor/public'; export const markdownVisDefinition = { name: 'markdown', diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx b/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx rename to src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.tsx b/src/plugins/vis_type_markdown/public/markdown_vis_controller.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.tsx rename to src/plugins/vis_type_markdown/public/markdown_vis_controller.tsx index 3260e9f7d8091..4e77bb196b713 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.tsx +++ b/src/plugins/vis_type_markdown/public/markdown_vis_controller.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { Markdown } from '../../../../plugins/kibana_react/public'; +import { Markdown } from '../../kibana_react/public'; import { MarkdownVisParams } from './types'; interface MarkdownVisComponentProps extends MarkdownVisParams { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts b/src/plugins/vis_type_markdown/public/plugin.ts similarity index 80% rename from src/legacy/core_plugins/vis_type_markdown/public/plugin.ts rename to src/plugins/vis_type_markdown/public/plugin.ts index 0445d270c9330..9365017a31adc 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts +++ b/src/plugins/vis_type_markdown/public/plugin.ts @@ -17,12 +17,15 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; import { markdownVisDefinition } from './markdown_vis'; import { createMarkdownVisFn } from './markdown_fn'; +import { ConfigSchema } from '../config'; + +import './index.scss'; /** @internal */ export interface MarkdownPluginSetupDependencies { @@ -32,9 +35,9 @@ export interface MarkdownPluginSetupDependencies { /** @internal */ export class MarkdownPlugin implements Plugin { - initializerContext: PluginInitializerContext; + initializerContext: PluginInitializerContext; - constructor(initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } diff --git a/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx b/src/plugins/vis_type_markdown/public/settings_options.tsx similarity index 95% rename from src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx rename to src/plugins/vis_type_markdown/public/settings_options.tsx index 16b2749c34e10..6f6a80564ce07 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/settings_options.tsx +++ b/src/plugins/vis_type_markdown/public/settings_options.tsx @@ -22,7 +22,7 @@ import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { RangeOption, SwitchOption } from '../../../../plugins/charts/public'; +import { RangeOption, SwitchOption } from '../../charts/public'; import { MarkdownVisParams } from './types'; function SettingsOptions({ stateParams, setValue }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/types.ts b/src/plugins/vis_type_markdown/public/types.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_markdown/public/types.ts rename to src/plugins/vis_type_markdown/public/types.ts diff --git a/src/plugins/timelion/public/index.ts b/src/plugins/vis_type_markdown/server/index.ts similarity index 69% rename from src/plugins/timelion/public/index.ts rename to src/plugins/vis_type_markdown/server/index.ts index b05c4f8a30b22..73e1712353b1a 100644 --- a/src/plugins/timelion/public/index.ts +++ b/src/plugins/vis_type_markdown/server/index.ts @@ -17,14 +17,18 @@ * under the License. */ -import { CoreStart, PluginInitializerContext } from 'kibana/public'; -import { ConfigSchema } from '../config'; +import { PluginConfigDescriptor } from 'kibana/server'; -export const plugin = (initializerContext: PluginInitializerContext) => ({ +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('markdown_vis.enabled', 'vis_type_markdown.enabled'), + ], +}; + +export const plugin = () => ({ setup() {}, - start(core: CoreStart) { - if (initializerContext.config.get().ui.enabled === false) { - core.chrome.navLinks.update('timelion', { hidden: true }); - } - }, + start() {}, }); diff --git a/src/legacy/core_plugins/vis_type_timelion/README.md b/src/plugins/vis_type_timelion/README.md similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/README.md rename to src/plugins/vis_type_timelion/README.md diff --git a/src/plugins/timelion/common/chain.peg b/src/plugins/vis_type_timelion/common/chain.peg similarity index 100% rename from src/plugins/timelion/common/chain.peg rename to src/plugins/vis_type_timelion/common/chain.peg diff --git a/src/plugins/timelion/common/lib/calculate_interval.test.ts b/src/plugins/vis_type_timelion/common/lib/calculate_interval.test.ts similarity index 100% rename from src/plugins/timelion/common/lib/calculate_interval.test.ts rename to src/plugins/vis_type_timelion/common/lib/calculate_interval.test.ts diff --git a/src/plugins/timelion/common/lib/calculate_interval.ts b/src/plugins/vis_type_timelion/common/lib/calculate_interval.ts similarity index 100% rename from src/plugins/timelion/common/lib/calculate_interval.ts rename to src/plugins/vis_type_timelion/common/lib/calculate_interval.ts diff --git a/src/plugins/timelion/common/lib/index.ts b/src/plugins/vis_type_timelion/common/lib/index.ts similarity index 100% rename from src/plugins/timelion/common/lib/index.ts rename to src/plugins/vis_type_timelion/common/lib/index.ts diff --git a/src/plugins/timelion/common/lib/to_milliseconds.ts b/src/plugins/vis_type_timelion/common/lib/to_milliseconds.ts similarity index 100% rename from src/plugins/timelion/common/lib/to_milliseconds.ts rename to src/plugins/vis_type_timelion/common/lib/to_milliseconds.ts diff --git a/src/plugins/timelion/common/types.ts b/src/plugins/vis_type_timelion/common/types.ts similarity index 100% rename from src/plugins/timelion/common/types.ts rename to src/plugins/vis_type_timelion/common/types.ts diff --git a/src/plugins/timelion/config.ts b/src/plugins/vis_type_timelion/config.ts similarity index 100% rename from src/plugins/timelion/config.ts rename to src/plugins/vis_type_timelion/config.ts diff --git a/src/plugins/vis_type_timelion/kibana.json b/src/plugins/vis_type_timelion/kibana.json new file mode 100644 index 0000000000000..85c282c51a2e7 --- /dev/null +++ b/src/plugins/vis_type_timelion/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "visTypeTimelion", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["visualizations", "data", "expressions"] +} diff --git a/src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js b/src/plugins/vis_type_timelion/public/_generated_/chain.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js rename to src/plugins/vis_type_timelion/public/_generated_/chain.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/_timelion_editor.scss b/src/plugins/vis_type_timelion/public/_timelion_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/_timelion_editor.scss rename to src/plugins/vis_type_timelion/public/_timelion_editor.scss diff --git a/src/legacy/core_plugins/vis_type_timelion/public/_timelion_vis.scss b/src/plugins/vis_type_timelion/public/_timelion_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/_timelion_vis.scss rename to src/plugins/vis_type_timelion/public/_timelion_vis.scss diff --git a/src/plugins/vis_type_timelion/public/components/_index.scss b/src/plugins/vis_type_timelion/public/components/_index.scss new file mode 100644 index 0000000000000..707c9dafebe2b --- /dev/null +++ b/src/plugins/vis_type_timelion/public/components/_index.scss @@ -0,0 +1,2 @@ +@import 'panel'; +@import 'timelion_expression_input'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/_panel.scss b/src/plugins/vis_type_timelion/public/components/_panel.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/components/_panel.scss rename to src/plugins/vis_type_timelion/public/components/_panel.scss diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/_timelion_expression_input.scss b/src/plugins/vis_type_timelion/public/components/_timelion_expression_input.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/components/_timelion_expression_input.scss rename to src/plugins/vis_type_timelion/public/components/_timelion_expression_input.scss diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/chart.tsx b/src/plugins/vis_type_timelion/public/components/chart.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/components/chart.tsx rename to src/plugins/vis_type_timelion/public/components/chart.tsx diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/index.ts b/src/plugins/vis_type_timelion/public/components/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/components/index.ts rename to src/plugins/vis_type_timelion/public/components/index.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/panel.tsx b/src/plugins/vis_type_timelion/public/components/panel.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_timelion/public/components/panel.tsx rename to src/plugins/vis_type_timelion/public/components/panel.tsx index 3b42fa7dfcbb8..8f796526e8520 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/panel.tsx +++ b/src/plugins/vis_type_timelion/public/components/panel.tsx @@ -22,9 +22,9 @@ import $ from 'jquery'; import moment from 'moment-timezone'; import { debounce, compact, get, each, cloneDeep, last, map } from 'lodash'; -import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../kibana_react/public'; import '../flot'; -import { DEFAULT_TIME_FORMAT } from '../../../../../plugins/timelion/common/lib'; +import { DEFAULT_TIME_FORMAT } from '../../common/lib'; import { buildSeriesData, diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input.tsx b/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input.tsx rename to src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx index c317451b8201e..999409ef35063 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx @@ -22,13 +22,10 @@ import { EuiFormLabel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { monaco } from '@kbn/ui-shared-deps/monaco'; -import { CodeEditor, useKibana } from '../../../../../plugins/kibana_react/public'; +import { CodeEditor, useKibana } from '../../../kibana_react/public'; import { suggest, getSuggestion } from './timelion_expression_input_helpers'; import { getArgValueSuggestions } from '../helpers/arg_value_suggestions'; -import { - ITimelionFunction, - TimelionFunctionArgs, -} from '../../../../../plugins/timelion/common/types'; +import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types'; const LANGUAGE_ID = 'timelion_expression'; monaco.languages.register({ id: LANGUAGE_ID }); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts b/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts similarity index 99% rename from src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts rename to src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts index 2f99256e2a192..2ff6809d1c83d 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts +++ b/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts @@ -22,7 +22,7 @@ import { getArgValueSuggestions } from '../helpers/arg_value_suggestions'; import { setIndexPatterns, setSavedObjectsClient } from '../helpers/plugin_services'; import { IndexPatternsContract } from 'src/plugins/data/public'; import { SavedObjectsClient } from 'kibana/public'; -import { ITimelionFunction } from '../../../../../plugins/timelion/common/types'; +import { ITimelionFunction } from '../../common/types'; describe('Timelion expression suggestions', () => { setIndexPatterns({} as IndexPatternsContract); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts b/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts similarity index 98% rename from src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts rename to src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts index 6f23c864419eb..04cb54306c90e 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts +++ b/src/plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts @@ -27,10 +27,7 @@ import { Parser } from 'pegjs'; import { parse } from '../_generated_/chain'; import { ArgValueSuggestions, FunctionArg, Location } from '../helpers/arg_value_suggestions'; -import { - ITimelionFunction, - TimelionFunctionArgs, -} from '../../../../../plugins/timelion/common/types'; +import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types'; export enum SUGGESTION_TYPE { ARGUMENTS = 'arguments', diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx b/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx rename to src/plugins/vis_type_timelion/public/components/timelion_interval.tsx index 8a8e1b22fb78d..985ecaeaf3e5a 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx @@ -21,9 +21,9 @@ import React, { useMemo, useCallback } from 'react'; import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { search } from '../../../../../plugins/data/public'; +import { search } from '../../../data/public'; const { isValidEsInterval } = search.aggs; -import { useValidation } from '../../../../../plugins/vis_default_editor/public'; +import { useValidation } from '../../../vis_default_editor/public'; const intervalOptions = [ { diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_vis.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx similarity index 95% rename from src/legacy/core_plugins/vis_type_timelion/public/components/timelion_vis.tsx rename to src/plugins/vis_type_timelion/public/components/timelion_vis.tsx index 0fad0a164bf0b..4bb07fe74ee82 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_vis.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx @@ -23,7 +23,7 @@ import { IUiSettingsClient } from 'kibana/public'; import { ChartComponent } from './chart'; import { VisParams } from '../timelion_vis_fn'; import { TimelionSuccessResponse } from '../helpers/timelion_request_handler'; -import { ExprVis } from '../../../../../plugins/visualizations/public'; +import { ExprVis } from '../../../visualizations/public'; export interface TimelionVisComponentProp { config: IUiSettingsClient; diff --git a/src/plugins/vis_type_timelion/public/flot.js b/src/plugins/vis_type_timelion/public/flot.js new file mode 100644 index 0000000000000..1ccb40c93a3d6 --- /dev/null +++ b/src/plugins/vis_type_timelion/public/flot.js @@ -0,0 +1,26 @@ +/* + * 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 './webpackShims/jquery.flot'; +import './webpackShims/jquery.flot.time'; +import './webpackShims/jquery.flot.symbol'; +import './webpackShims/jquery.flot.crosshair'; +import './webpackShims/jquery.flot.selection'; +import './webpackShims/jquery.flot.stack'; +import './webpackShims/jquery.flot.axislabels'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts b/src/plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts similarity index 97% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts rename to src/plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts index ea9532964d6fe..76c25b9b9e8de 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts +++ b/src/plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts @@ -19,11 +19,8 @@ import { get } from 'lodash'; import { getIndexPatterns, getSavedObjectsClient } from './plugin_services'; -import { TimelionFunctionArgs } from '../../../../../plugins/timelion/common/types'; -import { - indexPatterns as indexPatternsUtils, - IndexPatternAttributes, -} from '../../../../../plugins/data/public'; +import { TimelionFunctionArgs } from '../../common/types'; +import { indexPatterns as indexPatternsUtils, IndexPatternAttributes } from '../../../data/public'; export interface Location { min: number; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/get_timezone.ts b/src/plugins/vis_type_timelion/public/helpers/get_timezone.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/get_timezone.ts rename to src/plugins/vis_type_timelion/public/helpers/get_timezone.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/panel_utils.ts b/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts similarity index 98% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/panel_utils.ts rename to src/plugins/vis_type_timelion/public/helpers/panel_utils.ts index f932e5ee4b2f4..db29d9112be8e 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/panel_utils.ts +++ b/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts @@ -23,7 +23,7 @@ import moment, { Moment } from 'moment-timezone'; import { TimefilterContract } from 'src/plugins/data/public'; import { IUiSettingsClient } from 'kibana/public'; -import { calculateInterval } from '../../../../../plugins/timelion/common/lib'; +import { calculateInterval } from '../../common/lib'; import { xaxisFormatterProvider } from './xaxis_formatter'; import { Series } from './timelion_request_handler'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/plugin_services.ts b/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/plugin_services.ts rename to src/plugins/vis_type_timelion/public/helpers/plugin_services.ts index 5ba4ee5e47983..b055626934eea 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/plugin_services.ts +++ b/src/plugins/vis_type_timelion/public/helpers/plugin_services.ts @@ -19,7 +19,7 @@ import { IndexPatternsContract } from 'src/plugins/data/public'; import { SavedObjectsClientContract } from 'kibana/public'; -import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; +import { createGetterSetter } from '../../../kibana_utils/public'; export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( 'IndexPatterns' diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_formatters.test.ts b/src/plugins/vis_type_timelion/public/helpers/tick_formatters.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_formatters.test.ts rename to src/plugins/vis_type_timelion/public/helpers/tick_formatters.test.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_formatters.ts b/src/plugins/vis_type_timelion/public/helpers/tick_formatters.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_formatters.ts rename to src/plugins/vis_type_timelion/public/helpers/tick_formatters.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_generator.test.ts b/src/plugins/vis_type_timelion/public/helpers/tick_generator.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_generator.test.ts rename to src/plugins/vis_type_timelion/public/helpers/tick_generator.test.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_generator.ts b/src/plugins/vis_type_timelion/public/helpers/tick_generator.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/tick_generator.ts rename to src/plugins/vis_type_timelion/public/helpers/tick_generator.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts rename to src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts index 61e31420f73ba..a654f7935af5f 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts +++ b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { KIBANA_CONTEXT_NAME } from 'src/plugins/expressions/public'; -import { VisParams } from '../../../../../plugins/visualizations/public'; -import { TimeRange, Filter, esQuery, Query } from '../../../../../plugins/data/public'; +import { VisParams } from '../../../visualizations/public'; +import { TimeRange, Filter, esQuery, Query } from '../../../data/public'; import { TimelionVisDependencies } from '../plugin'; import { getTimezone } from './get_timezone'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/xaxis_formatter.ts b/src/plugins/vis_type_timelion/public/helpers/xaxis_formatter.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/helpers/xaxis_formatter.ts rename to src/plugins/vis_type_timelion/public/helpers/xaxis_formatter.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/index.scss b/src/plugins/vis_type_timelion/public/index.scss similarity index 59% rename from src/legacy/core_plugins/vis_type_timelion/public/index.scss rename to src/plugins/vis_type_timelion/public/index.scss index 313f14a8acf69..00e9a88520961 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/index.scss +++ b/src/plugins/vis_type_timelion/public/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - @import './timelion_vis'; @import './timelion_editor'; @import './components/index'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/index.ts b/src/plugins/vis_type_timelion/public/index.ts similarity index 90% rename from src/legacy/core_plugins/vis_type_timelion/public/index.ts rename to src/plugins/vis_type_timelion/public/index.ts index 6292e2ad3eb08..0aa5f3a810033 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/index.ts +++ b/src/plugins/vis_type_timelion/public/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../core/public'; +import { PluginInitializerContext } from 'kibana/public'; import { TimelionVisPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { @@ -25,3 +25,5 @@ export function plugin(initializerContext: PluginInitializerContext) { } export { getTimezone } from './helpers/get_timezone'; + +export { VisTypeTimelionPluginStart } from './plugin'; diff --git a/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts b/src/plugins/vis_type_timelion/public/plugin.ts similarity index 67% rename from src/legacy/core_plugins/vis_type_timelion/public/plugin.ts rename to src/plugins/vis_type_timelion/public/plugin.ts index b5aa64db19aa4..060fec04deb3f 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts +++ b/src/plugins/vis_type_timelion/public/plugin.ts @@ -26,16 +26,21 @@ import { HttpSetup, } from 'kibana/public'; import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public'; -import { DataPublicPluginSetup, TimefilterContract } from 'src/plugins/data/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, + TimefilterContract, +} from 'src/plugins/data/public'; -import { PluginsStart } from './legacy_imports'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; +import { VisualizationsSetup } from '../../visualizations/public'; import { getTimelionVisualizationConfig } from './timelion_vis_fn'; import { getTimelionVisDefinition } from './timelion_vis_type'; import { setIndexPatterns, setSavedObjectsClient } from './helpers/plugin_services'; +import { ConfigSchema } from '../config'; -type TimelionVisCoreSetup = CoreSetup; +import './index.scss'; +import { getArgValueSuggestions } from './helpers/arg_value_suggestions'; /** @internal */ export interface TimelionVisDependencies extends Partial { @@ -52,11 +57,28 @@ export interface TimelionVisSetupDependencies { } /** @internal */ -export class TimelionVisPlugin implements Plugin { - constructor(public initializerContext: PluginInitializerContext) {} +export interface TimelionVisStartDependencies { + data: DataPublicPluginStart; +} + +/** @public */ +export interface VisTypeTimelionPluginStart { + getArgValueSuggestions: typeof getArgValueSuggestions; +} + +/** @internal */ +export class TimelionVisPlugin + implements + Plugin< + void, + VisTypeTimelionPluginStart, + TimelionVisSetupDependencies, + TimelionVisStartDependencies + > { + constructor(public initializerContext: PluginInitializerContext) {} - public async setup( - core: TimelionVisCoreSetup, + public setup( + core: CoreSetup, { expressions, visualizations, data }: TimelionVisSetupDependencies ) { const dependencies: TimelionVisDependencies = { @@ -69,8 +91,15 @@ export class TimelionVisPlugin implements Plugin { visualizations.createReactVisualization(getTimelionVisDefinition(dependencies)); } - public start(core: CoreStart, plugins: PluginsStart) { + public start(core: CoreStart, plugins: TimelionVisStartDependencies) { setIndexPatterns(plugins.data.indexPatterns); setSavedObjectsClient(core.savedObjects.client); + if (this.initializerContext.config.get().ui.enabled === false) { + core.chrome.navLinks.update('timelion', { hidden: true }); + } + + return { + getArgValueSuggestions, + }; } } diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx b/src/plugins/vis_type_timelion/public/timelion_options.tsx similarity index 95% rename from src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx rename to src/plugins/vis_type_timelion/public/timelion_options.tsx index afffcf7ccaf7a..dfe017d3a273f 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/timelion_options.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_options.tsx @@ -24,7 +24,9 @@ import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { VisParams } from './timelion_vis_fn'; import { TimelionInterval, TimelionExpressionInput } from './components'; -function TimelionOptions({ stateParams, setValue, setValidity }: VisOptionsProps) { +export type TimelionOptionsProps = VisOptionsProps; + +function TimelionOptions({ stateParams, setValue, setValidity }: TimelionOptionsProps) { const setInterval = useCallback((value: VisParams['interval']) => setValue('interval', value), [ setValue, ]); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_fn.ts rename to src/plugins/vis_type_timelion/public/timelion_vis_fn.ts diff --git a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx similarity index 84% rename from src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx rename to src/plugins/vis_type_timelion/public/timelion_vis_type.tsx index 5be77b3e51a6a..52addb3c2d9d2 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/timelion_vis_type.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx @@ -20,11 +20,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { KibanaContextProvider } from '../../../../plugins/kibana_react/public'; -import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; +import { KibanaContextProvider } from '../../kibana_react/public'; +import { DefaultEditorSize } from '../../vis_default_editor/public'; import { getTimelionRequestHandler } from './helpers/timelion_request_handler'; import { TimelionVisComponent, TimelionVisComponentProp } from './components'; -import { TimelionOptions } from './timelion_options'; +import { TimelionOptions, TimelionOptionsProps } from './timelion_options'; import { TimelionVisDependencies } from './plugin'; export const TIMELION_VIS_NAME = 'timelion'; @@ -53,7 +53,11 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) ), }, editorConfig: { - optionsTemplate: TimelionOptions, + optionsTemplate: (props: TimelionOptionsProps) => ( + + + + ), defaultSize: DefaultEditorSize.MEDIUM, }, requestHandler: timelionRequestHandler, diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.axislabels.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.axislabels.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.axislabels.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.axislabels.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.crosshair.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.crosshair.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.crosshair.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.crosshair.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.selection.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.selection.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.selection.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.selection.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.stack.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.stack.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.stack.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.stack.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.symbol.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.symbol.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.symbol.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.symbol.js diff --git a/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.time.js b/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.time.js similarity index 100% rename from src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.time.js rename to src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.time.js diff --git a/src/plugins/timelion/server/fit_functions/average.js b/src/plugins/vis_type_timelion/server/fit_functions/average.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/average.js rename to src/plugins/vis_type_timelion/server/fit_functions/average.js diff --git a/src/plugins/timelion/server/fit_functions/average.test.js b/src/plugins/vis_type_timelion/server/fit_functions/average.test.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/average.test.js rename to src/plugins/vis_type_timelion/server/fit_functions/average.test.js diff --git a/src/plugins/timelion/server/fit_functions/carry.js b/src/plugins/vis_type_timelion/server/fit_functions/carry.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/carry.js rename to src/plugins/vis_type_timelion/server/fit_functions/carry.js diff --git a/src/plugins/timelion/server/fit_functions/carry.test.js b/src/plugins/vis_type_timelion/server/fit_functions/carry.test.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/carry.test.js rename to src/plugins/vis_type_timelion/server/fit_functions/carry.test.js diff --git a/src/plugins/timelion/server/fit_functions/nearest.js b/src/plugins/vis_type_timelion/server/fit_functions/nearest.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/nearest.js rename to src/plugins/vis_type_timelion/server/fit_functions/nearest.js diff --git a/src/plugins/timelion/server/fit_functions/none.js b/src/plugins/vis_type_timelion/server/fit_functions/none.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/none.js rename to src/plugins/vis_type_timelion/server/fit_functions/none.js diff --git a/src/plugins/timelion/server/fit_functions/scale.js b/src/plugins/vis_type_timelion/server/fit_functions/scale.js similarity index 100% rename from src/plugins/timelion/server/fit_functions/scale.js rename to src/plugins/vis_type_timelion/server/fit_functions/scale.js diff --git a/src/plugins/timelion/server/handlers/chain_runner.js b/src/plugins/vis_type_timelion/server/handlers/chain_runner.js similarity index 100% rename from src/plugins/timelion/server/handlers/chain_runner.js rename to src/plugins/vis_type_timelion/server/handlers/chain_runner.js diff --git a/src/plugins/timelion/server/handlers/lib/arg_type.js b/src/plugins/vis_type_timelion/server/handlers/lib/arg_type.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/arg_type.js rename to src/plugins/vis_type_timelion/server/handlers/lib/arg_type.js diff --git a/src/plugins/timelion/server/handlers/lib/index_arguments.js b/src/plugins/vis_type_timelion/server/handlers/lib/index_arguments.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/index_arguments.js rename to src/plugins/vis_type_timelion/server/handlers/lib/index_arguments.js diff --git a/src/plugins/timelion/server/handlers/lib/parse_sheet.js b/src/plugins/vis_type_timelion/server/handlers/lib/parse_sheet.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/parse_sheet.js rename to src/plugins/vis_type_timelion/server/handlers/lib/parse_sheet.js diff --git a/src/plugins/timelion/server/handlers/lib/parse_sheet.test.js b/src/plugins/vis_type_timelion/server/handlers/lib/parse_sheet.test.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/parse_sheet.test.js rename to src/plugins/vis_type_timelion/server/handlers/lib/parse_sheet.test.js diff --git a/src/plugins/timelion/server/handlers/lib/preprocess_chain.js b/src/plugins/vis_type_timelion/server/handlers/lib/preprocess_chain.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/preprocess_chain.js rename to src/plugins/vis_type_timelion/server/handlers/lib/preprocess_chain.js diff --git a/src/plugins/timelion/server/handlers/lib/reposition_arguments.js b/src/plugins/vis_type_timelion/server/handlers/lib/reposition_arguments.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/reposition_arguments.js rename to src/plugins/vis_type_timelion/server/handlers/lib/reposition_arguments.js diff --git a/src/plugins/timelion/server/handlers/lib/tl_config.js b/src/plugins/vis_type_timelion/server/handlers/lib/tl_config.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/tl_config.js rename to src/plugins/vis_type_timelion/server/handlers/lib/tl_config.js diff --git a/src/plugins/timelion/server/handlers/lib/validate_arg.js b/src/plugins/vis_type_timelion/server/handlers/lib/validate_arg.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/validate_arg.js rename to src/plugins/vis_type_timelion/server/handlers/lib/validate_arg.js diff --git a/src/plugins/timelion/server/handlers/lib/validate_time.js b/src/plugins/vis_type_timelion/server/handlers/lib/validate_time.js similarity index 100% rename from src/plugins/timelion/server/handlers/lib/validate_time.js rename to src/plugins/vis_type_timelion/server/handlers/lib/validate_time.js diff --git a/src/plugins/timelion/server/index.ts b/src/plugins/vis_type_timelion/server/index.ts similarity index 64% rename from src/plugins/timelion/server/index.ts rename to src/plugins/vis_type_timelion/server/index.ts index 5d420327f961e..b40ab2af2b0d7 100644 --- a/src/plugins/timelion/server/index.ts +++ b/src/plugins/vis_type_timelion/server/index.ts @@ -17,19 +17,23 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../src/core/server'; -import { configSchema } from '../config'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; +import { configSchema, ConfigSchema } from '../config'; import { Plugin } from './plugin'; export { PluginSetupContract } from './plugin'; -export const config = { +export const config: PluginConfigDescriptor = { schema: configSchema, exposeToBrowser: { - ui: { - enabled: true, - }, + ui: true, }, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('timelion_vis.enabled', 'vis_type_timelion.enabled'), + renameFromRoot('timelion.enabled', 'vis_type_timelion.enabled'), + renameFromRoot('timelion.graphiteUrls', 'vis_type_timelion.graphiteUrls'), + renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled'), + ], }; export const plugin = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); diff --git a/src/plugins/timelion/server/lib/alter.js b/src/plugins/vis_type_timelion/server/lib/alter.js similarity index 100% rename from src/plugins/timelion/server/lib/alter.js rename to src/plugins/vis_type_timelion/server/lib/alter.js diff --git a/src/plugins/timelion/server/lib/as_sorted.js b/src/plugins/vis_type_timelion/server/lib/as_sorted.js similarity index 100% rename from src/plugins/timelion/server/lib/as_sorted.js rename to src/plugins/vis_type_timelion/server/lib/as_sorted.js diff --git a/src/plugins/timelion/server/lib/build_target.js b/src/plugins/vis_type_timelion/server/lib/build_target.js similarity index 100% rename from src/plugins/timelion/server/lib/build_target.js rename to src/plugins/vis_type_timelion/server/lib/build_target.js diff --git a/src/plugins/timelion/server/lib/classes/chainable.js b/src/plugins/vis_type_timelion/server/lib/classes/chainable.js similarity index 100% rename from src/plugins/timelion/server/lib/classes/chainable.js rename to src/plugins/vis_type_timelion/server/lib/classes/chainable.js diff --git a/src/plugins/timelion/server/lib/classes/datasource.js b/src/plugins/vis_type_timelion/server/lib/classes/datasource.js similarity index 100% rename from src/plugins/timelion/server/lib/classes/datasource.js rename to src/plugins/vis_type_timelion/server/lib/classes/datasource.js diff --git a/src/plugins/timelion/server/lib/classes/timelion_function.d.ts b/src/plugins/vis_type_timelion/server/lib/classes/timelion_function.d.ts similarity index 100% rename from src/plugins/timelion/server/lib/classes/timelion_function.d.ts rename to src/plugins/vis_type_timelion/server/lib/classes/timelion_function.d.ts diff --git a/src/plugins/timelion/server/lib/classes/timelion_function.js b/src/plugins/vis_type_timelion/server/lib/classes/timelion_function.js similarity index 100% rename from src/plugins/timelion/server/lib/classes/timelion_function.js rename to src/plugins/vis_type_timelion/server/lib/classes/timelion_function.js diff --git a/src/plugins/timelion/server/lib/config_manager.ts b/src/plugins/vis_type_timelion/server/lib/config_manager.ts similarity index 100% rename from src/plugins/timelion/server/lib/config_manager.ts rename to src/plugins/vis_type_timelion/server/lib/config_manager.ts diff --git a/src/plugins/timelion/server/lib/functions_md.js b/src/plugins/vis_type_timelion/server/lib/functions_md.js similarity index 100% rename from src/plugins/timelion/server/lib/functions_md.js rename to src/plugins/vis_type_timelion/server/lib/functions_md.js diff --git a/src/plugins/timelion/server/lib/get_namespaced_settings.js b/src/plugins/vis_type_timelion/server/lib/get_namespaced_settings.js similarity index 100% rename from src/plugins/timelion/server/lib/get_namespaced_settings.js rename to src/plugins/vis_type_timelion/server/lib/get_namespaced_settings.js diff --git a/src/plugins/timelion/server/lib/load_functions.d.ts b/src/plugins/vis_type_timelion/server/lib/load_functions.d.ts similarity index 100% rename from src/plugins/timelion/server/lib/load_functions.d.ts rename to src/plugins/vis_type_timelion/server/lib/load_functions.d.ts diff --git a/src/plugins/timelion/server/lib/load_functions.js b/src/plugins/vis_type_timelion/server/lib/load_functions.js similarity index 100% rename from src/plugins/timelion/server/lib/load_functions.js rename to src/plugins/vis_type_timelion/server/lib/load_functions.js diff --git a/src/plugins/timelion/server/lib/load_functions.test.js b/src/plugins/vis_type_timelion/server/lib/load_functions.test.js similarity index 94% rename from src/plugins/timelion/server/lib/load_functions.test.js rename to src/plugins/vis_type_timelion/server/lib/load_functions.test.js index ebe1a04532e05..b4f83611a7773 100644 --- a/src/plugins/timelion/server/lib/load_functions.test.js +++ b/src/plugins/vis_type_timelion/server/lib/load_functions.test.js @@ -17,7 +17,7 @@ * under the License. */ -const fn = require(`src/plugins/timelion/server/lib/load_functions`); +const fn = require(`src/plugins/vis_type_timelion/server/lib/load_functions`); const expect = require('chai').expect; diff --git a/src/plugins/timelion/server/lib/offset_time.js b/src/plugins/vis_type_timelion/server/lib/offset_time.js similarity index 100% rename from src/plugins/timelion/server/lib/offset_time.js rename to src/plugins/vis_type_timelion/server/lib/offset_time.js diff --git a/src/plugins/timelion/server/lib/offset_time.test.js b/src/plugins/vis_type_timelion/server/lib/offset_time.test.js similarity index 100% rename from src/plugins/timelion/server/lib/offset_time.test.js rename to src/plugins/vis_type_timelion/server/lib/offset_time.test.js diff --git a/src/plugins/timelion/server/lib/process_function_definition.js b/src/plugins/vis_type_timelion/server/lib/process_function_definition.js similarity index 100% rename from src/plugins/timelion/server/lib/process_function_definition.js rename to src/plugins/vis_type_timelion/server/lib/process_function_definition.js diff --git a/src/plugins/timelion/server/lib/reduce.js b/src/plugins/vis_type_timelion/server/lib/reduce.js similarity index 100% rename from src/plugins/timelion/server/lib/reduce.js rename to src/plugins/vis_type_timelion/server/lib/reduce.js diff --git a/src/plugins/timelion/server/lib/split_interval.js b/src/plugins/vis_type_timelion/server/lib/split_interval.js similarity index 100% rename from src/plugins/timelion/server/lib/split_interval.js rename to src/plugins/vis_type_timelion/server/lib/split_interval.js diff --git a/src/plugins/timelion/server/lib/unzip_pairs.js b/src/plugins/vis_type_timelion/server/lib/unzip_pairs.js similarity index 100% rename from src/plugins/timelion/server/lib/unzip_pairs.js rename to src/plugins/vis_type_timelion/server/lib/unzip_pairs.js diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts similarity index 100% rename from src/plugins/timelion/server/plugin.ts rename to src/plugins/vis_type_timelion/server/plugin.ts diff --git a/src/plugins/timelion/server/routes/functions.ts b/src/plugins/vis_type_timelion/server/routes/functions.ts similarity index 100% rename from src/plugins/timelion/server/routes/functions.ts rename to src/plugins/vis_type_timelion/server/routes/functions.ts diff --git a/src/plugins/timelion/server/routes/run.ts b/src/plugins/vis_type_timelion/server/routes/run.ts similarity index 100% rename from src/plugins/timelion/server/routes/run.ts rename to src/plugins/vis_type_timelion/server/routes/run.ts diff --git a/src/plugins/timelion/server/routes/validate_es.ts b/src/plugins/vis_type_timelion/server/routes/validate_es.ts similarity index 100% rename from src/plugins/timelion/server/routes/validate_es.ts rename to src/plugins/vis_type_timelion/server/routes/validate_es.ts diff --git a/src/plugins/timelion/server/series_functions/abs.js b/src/plugins/vis_type_timelion/server/series_functions/abs.js similarity index 100% rename from src/plugins/timelion/server/series_functions/abs.js rename to src/plugins/vis_type_timelion/server/series_functions/abs.js diff --git a/src/plugins/timelion/server/series_functions/abs.test.js b/src/plugins/vis_type_timelion/server/series_functions/abs.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/abs.test.js rename to src/plugins/vis_type_timelion/server/series_functions/abs.test.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/aggregate.test.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/aggregate.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/aggregate.test.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/aggregate.test.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/avg.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/avg.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/avg.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/avg.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/cardinality.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/cardinality.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/cardinality.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/cardinality.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/first.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/first.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/first.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/first.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/index.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/index.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/index.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/index.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/last.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/last.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/last.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/last.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/max.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/max.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/max.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/max.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/min.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/min.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/min.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/min.js diff --git a/src/plugins/timelion/server/series_functions/aggregate/sum.js b/src/plugins/vis_type_timelion/server/series_functions/aggregate/sum.js similarity index 100% rename from src/plugins/timelion/server/series_functions/aggregate/sum.js rename to src/plugins/vis_type_timelion/server/series_functions/aggregate/sum.js diff --git a/src/plugins/timelion/server/series_functions/bars.js b/src/plugins/vis_type_timelion/server/series_functions/bars.js similarity index 100% rename from src/plugins/timelion/server/series_functions/bars.js rename to src/plugins/vis_type_timelion/server/series_functions/bars.js diff --git a/src/plugins/timelion/server/series_functions/bars.test.js b/src/plugins/vis_type_timelion/server/series_functions/bars.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/bars.test.js rename to src/plugins/vis_type_timelion/server/series_functions/bars.test.js diff --git a/src/plugins/timelion/server/series_functions/color.js b/src/plugins/vis_type_timelion/server/series_functions/color.js similarity index 100% rename from src/plugins/timelion/server/series_functions/color.js rename to src/plugins/vis_type_timelion/server/series_functions/color.js diff --git a/src/plugins/timelion/server/series_functions/color.test.js b/src/plugins/vis_type_timelion/server/series_functions/color.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/color.test.js rename to src/plugins/vis_type_timelion/server/series_functions/color.test.js diff --git a/src/plugins/timelion/server/series_functions/condition.js b/src/plugins/vis_type_timelion/server/series_functions/condition.js similarity index 100% rename from src/plugins/timelion/server/series_functions/condition.js rename to src/plugins/vis_type_timelion/server/series_functions/condition.js diff --git a/src/plugins/timelion/server/series_functions/condition.test.js b/src/plugins/vis_type_timelion/server/series_functions/condition.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/condition.test.js rename to src/plugins/vis_type_timelion/server/series_functions/condition.test.js diff --git a/src/plugins/timelion/server/series_functions/cusum.js b/src/plugins/vis_type_timelion/server/series_functions/cusum.js similarity index 100% rename from src/plugins/timelion/server/series_functions/cusum.js rename to src/plugins/vis_type_timelion/server/series_functions/cusum.js diff --git a/src/plugins/timelion/server/series_functions/cusum.test.js b/src/plugins/vis_type_timelion/server/series_functions/cusum.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/cusum.test.js rename to src/plugins/vis_type_timelion/server/series_functions/cusum.test.js diff --git a/src/plugins/timelion/server/series_functions/derivative.js b/src/plugins/vis_type_timelion/server/series_functions/derivative.js similarity index 100% rename from src/plugins/timelion/server/series_functions/derivative.js rename to src/plugins/vis_type_timelion/server/series_functions/derivative.js diff --git a/src/plugins/timelion/server/series_functions/derivative.test.js b/src/plugins/vis_type_timelion/server/series_functions/derivative.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/derivative.test.js rename to src/plugins/vis_type_timelion/server/series_functions/derivative.test.js diff --git a/src/plugins/timelion/server/series_functions/divide.js b/src/plugins/vis_type_timelion/server/series_functions/divide.js similarity index 100% rename from src/plugins/timelion/server/series_functions/divide.js rename to src/plugins/vis_type_timelion/server/series_functions/divide.js diff --git a/src/plugins/timelion/server/series_functions/divide.test.js b/src/plugins/vis_type_timelion/server/series_functions/divide.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/divide.test.js rename to src/plugins/vis_type_timelion/server/series_functions/divide.test.js diff --git a/src/plugins/timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/es.test.js rename to src/plugins/vis_type_timelion/server/series_functions/es/es.test.js diff --git a/src/plugins/timelion/server/series_functions/es/index.js b/src/plugins/vis_type_timelion/server/series_functions/es/index.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/index.js rename to src/plugins/vis_type_timelion/server/series_functions/es/index.js diff --git a/src/plugins/timelion/server/series_functions/es/lib/agg_body.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_body.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/lib/agg_body.js rename to src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_body.js diff --git a/src/plugins/timelion/server/series_functions/es/lib/agg_response_to_series_list.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_response_to_series_list.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/lib/agg_response_to_series_list.js rename to src/plugins/vis_type_timelion/server/series_functions/es/lib/agg_response_to_series_list.js diff --git a/src/plugins/timelion/server/series_functions/es/lib/build_request.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/lib/build_request.js rename to src/plugins/vis_type_timelion/server/series_functions/es/lib/build_request.js diff --git a/src/plugins/timelion/server/series_functions/es/lib/create_date_agg.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/create_date_agg.js similarity index 100% rename from src/plugins/timelion/server/series_functions/es/lib/create_date_agg.js rename to src/plugins/vis_type_timelion/server/series_functions/es/lib/create_date_agg.js diff --git a/src/plugins/timelion/server/series_functions/first.js b/src/plugins/vis_type_timelion/server/series_functions/first.js similarity index 100% rename from src/plugins/timelion/server/series_functions/first.js rename to src/plugins/vis_type_timelion/server/series_functions/first.js diff --git a/src/plugins/timelion/server/series_functions/first.test.js b/src/plugins/vis_type_timelion/server/series_functions/first.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/first.test.js rename to src/plugins/vis_type_timelion/server/series_functions/first.test.js diff --git a/src/plugins/timelion/server/series_functions/fit.js b/src/plugins/vis_type_timelion/server/series_functions/fit.js similarity index 100% rename from src/plugins/timelion/server/series_functions/fit.js rename to src/plugins/vis_type_timelion/server/series_functions/fit.js diff --git a/src/plugins/timelion/server/series_functions/fit.test.js b/src/plugins/vis_type_timelion/server/series_functions/fit.test.js similarity index 98% rename from src/plugins/timelion/server/series_functions/fit.test.js rename to src/plugins/vis_type_timelion/server/series_functions/fit.test.js index 75eaa2a50ea72..6622259a1fd87 100644 --- a/src/plugins/timelion/server/series_functions/fit.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/fit.test.js @@ -17,7 +17,7 @@ * under the License. */ -const fn = require(`src/plugins/timelion/server/series_functions/fit`); +const fn = require(`src/plugins/vis_type_timelion/server/series_functions/fit`); import moment from 'moment'; const expect = require('chai').expect; import invoke from './helpers/invoke_series_fn.js'; diff --git a/src/plugins/timelion/server/series_functions/fixtures/bucket_list.js b/src/plugins/vis_type_timelion/server/series_functions/fixtures/bucket_list.js similarity index 100% rename from src/plugins/timelion/server/series_functions/fixtures/bucket_list.js rename to src/plugins/vis_type_timelion/server/series_functions/fixtures/bucket_list.js diff --git a/src/plugins/timelion/server/series_functions/fixtures/es_response.js b/src/plugins/vis_type_timelion/server/series_functions/fixtures/es_response.js similarity index 100% rename from src/plugins/timelion/server/series_functions/fixtures/es_response.js rename to src/plugins/vis_type_timelion/server/series_functions/fixtures/es_response.js diff --git a/src/plugins/timelion/server/series_functions/fixtures/series_list.js b/src/plugins/vis_type_timelion/server/series_functions/fixtures/series_list.js similarity index 100% rename from src/plugins/timelion/server/series_functions/fixtures/series_list.js rename to src/plugins/vis_type_timelion/server/series_functions/fixtures/series_list.js diff --git a/src/plugins/timelion/server/series_functions/fixtures/tl_config.js b/src/plugins/vis_type_timelion/server/series_functions/fixtures/tl_config.js similarity index 100% rename from src/plugins/timelion/server/series_functions/fixtures/tl_config.js rename to src/plugins/vis_type_timelion/server/series_functions/fixtures/tl_config.js diff --git a/src/plugins/timelion/server/series_functions/graphite.js b/src/plugins/vis_type_timelion/server/series_functions/graphite.js similarity index 100% rename from src/plugins/timelion/server/series_functions/graphite.js rename to src/plugins/vis_type_timelion/server/series_functions/graphite.js diff --git a/src/plugins/timelion/server/series_functions/graphite.test.js b/src/plugins/vis_type_timelion/server/series_functions/graphite.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/graphite.test.js rename to src/plugins/vis_type_timelion/server/series_functions/graphite.test.js diff --git a/src/plugins/timelion/server/series_functions/helpers/get_series.js b/src/plugins/vis_type_timelion/server/series_functions/helpers/get_series.js similarity index 100% rename from src/plugins/timelion/server/series_functions/helpers/get_series.js rename to src/plugins/vis_type_timelion/server/series_functions/helpers/get_series.js diff --git a/src/plugins/timelion/server/series_functions/helpers/get_series_list.js b/src/plugins/vis_type_timelion/server/series_functions/helpers/get_series_list.js similarity index 100% rename from src/plugins/timelion/server/series_functions/helpers/get_series_list.js rename to src/plugins/vis_type_timelion/server/series_functions/helpers/get_series_list.js diff --git a/src/plugins/timelion/server/series_functions/helpers/get_single_series_list.js b/src/plugins/vis_type_timelion/server/series_functions/helpers/get_single_series_list.js similarity index 100% rename from src/plugins/timelion/server/series_functions/helpers/get_single_series_list.js rename to src/plugins/vis_type_timelion/server/series_functions/helpers/get_single_series_list.js diff --git a/src/plugins/timelion/server/series_functions/helpers/invoke_series_fn.js b/src/plugins/vis_type_timelion/server/series_functions/helpers/invoke_series_fn.js similarity index 100% rename from src/plugins/timelion/server/series_functions/helpers/invoke_series_fn.js rename to src/plugins/vis_type_timelion/server/series_functions/helpers/invoke_series_fn.js diff --git a/src/plugins/timelion/server/series_functions/hide.js b/src/plugins/vis_type_timelion/server/series_functions/hide.js similarity index 100% rename from src/plugins/timelion/server/series_functions/hide.js rename to src/plugins/vis_type_timelion/server/series_functions/hide.js diff --git a/src/plugins/timelion/server/series_functions/hide.test.js b/src/plugins/vis_type_timelion/server/series_functions/hide.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/hide.test.js rename to src/plugins/vis_type_timelion/server/series_functions/hide.test.js diff --git a/src/plugins/timelion/server/series_functions/holt/index.js b/src/plugins/vis_type_timelion/server/series_functions/holt/index.js similarity index 100% rename from src/plugins/timelion/server/series_functions/holt/index.js rename to src/plugins/vis_type_timelion/server/series_functions/holt/index.js diff --git a/src/plugins/timelion/server/series_functions/holt/lib/des.js b/src/plugins/vis_type_timelion/server/series_functions/holt/lib/des.js similarity index 100% rename from src/plugins/timelion/server/series_functions/holt/lib/des.js rename to src/plugins/vis_type_timelion/server/series_functions/holt/lib/des.js diff --git a/src/plugins/timelion/server/series_functions/holt/lib/ses.js b/src/plugins/vis_type_timelion/server/series_functions/holt/lib/ses.js similarity index 100% rename from src/plugins/timelion/server/series_functions/holt/lib/ses.js rename to src/plugins/vis_type_timelion/server/series_functions/holt/lib/ses.js diff --git a/src/plugins/timelion/server/series_functions/holt/lib/tes.js b/src/plugins/vis_type_timelion/server/series_functions/holt/lib/tes.js similarity index 100% rename from src/plugins/timelion/server/series_functions/holt/lib/tes.js rename to src/plugins/vis_type_timelion/server/series_functions/holt/lib/tes.js diff --git a/src/plugins/timelion/server/series_functions/label.js b/src/plugins/vis_type_timelion/server/series_functions/label.js similarity index 100% rename from src/plugins/timelion/server/series_functions/label.js rename to src/plugins/vis_type_timelion/server/series_functions/label.js diff --git a/src/plugins/timelion/server/series_functions/label.test.js b/src/plugins/vis_type_timelion/server/series_functions/label.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/label.test.js rename to src/plugins/vis_type_timelion/server/series_functions/label.test.js diff --git a/src/plugins/timelion/server/series_functions/legend.js b/src/plugins/vis_type_timelion/server/series_functions/legend.js similarity index 100% rename from src/plugins/timelion/server/series_functions/legend.js rename to src/plugins/vis_type_timelion/server/series_functions/legend.js diff --git a/src/plugins/timelion/server/series_functions/legend.test.js b/src/plugins/vis_type_timelion/server/series_functions/legend.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/legend.test.js rename to src/plugins/vis_type_timelion/server/series_functions/legend.test.js diff --git a/src/plugins/timelion/server/series_functions/lines.js b/src/plugins/vis_type_timelion/server/series_functions/lines.js similarity index 100% rename from src/plugins/timelion/server/series_functions/lines.js rename to src/plugins/vis_type_timelion/server/series_functions/lines.js diff --git a/src/plugins/timelion/server/series_functions/lines.test.js b/src/plugins/vis_type_timelion/server/series_functions/lines.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/lines.test.js rename to src/plugins/vis_type_timelion/server/series_functions/lines.test.js diff --git a/src/plugins/timelion/server/series_functions/log.js b/src/plugins/vis_type_timelion/server/series_functions/log.js similarity index 100% rename from src/plugins/timelion/server/series_functions/log.js rename to src/plugins/vis_type_timelion/server/series_functions/log.js diff --git a/src/plugins/timelion/server/series_functions/log.test.js b/src/plugins/vis_type_timelion/server/series_functions/log.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/log.test.js rename to src/plugins/vis_type_timelion/server/series_functions/log.test.js diff --git a/src/plugins/timelion/server/series_functions/max.js b/src/plugins/vis_type_timelion/server/series_functions/max.js similarity index 100% rename from src/plugins/timelion/server/series_functions/max.js rename to src/plugins/vis_type_timelion/server/series_functions/max.js diff --git a/src/plugins/timelion/server/series_functions/max.test.js b/src/plugins/vis_type_timelion/server/series_functions/max.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/max.test.js rename to src/plugins/vis_type_timelion/server/series_functions/max.test.js diff --git a/src/plugins/timelion/server/series_functions/min.js b/src/plugins/vis_type_timelion/server/series_functions/min.js similarity index 100% rename from src/plugins/timelion/server/series_functions/min.js rename to src/plugins/vis_type_timelion/server/series_functions/min.js diff --git a/src/plugins/timelion/server/series_functions/min.test.js b/src/plugins/vis_type_timelion/server/series_functions/min.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/min.test.js rename to src/plugins/vis_type_timelion/server/series_functions/min.test.js diff --git a/src/plugins/timelion/server/series_functions/movingaverage.js b/src/plugins/vis_type_timelion/server/series_functions/movingaverage.js similarity index 100% rename from src/plugins/timelion/server/series_functions/movingaverage.js rename to src/plugins/vis_type_timelion/server/series_functions/movingaverage.js diff --git a/src/plugins/timelion/server/series_functions/movingaverage.test.js b/src/plugins/vis_type_timelion/server/series_functions/movingaverage.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/movingaverage.test.js rename to src/plugins/vis_type_timelion/server/series_functions/movingaverage.test.js diff --git a/src/plugins/timelion/server/series_functions/movingstd.js b/src/plugins/vis_type_timelion/server/series_functions/movingstd.js similarity index 100% rename from src/plugins/timelion/server/series_functions/movingstd.js rename to src/plugins/vis_type_timelion/server/series_functions/movingstd.js diff --git a/src/plugins/timelion/server/series_functions/movingstd.test.js b/src/plugins/vis_type_timelion/server/series_functions/movingstd.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/movingstd.test.js rename to src/plugins/vis_type_timelion/server/series_functions/movingstd.test.js diff --git a/src/plugins/timelion/server/series_functions/multiply.js b/src/plugins/vis_type_timelion/server/series_functions/multiply.js similarity index 100% rename from src/plugins/timelion/server/series_functions/multiply.js rename to src/plugins/vis_type_timelion/server/series_functions/multiply.js diff --git a/src/plugins/timelion/server/series_functions/multiply.test.js b/src/plugins/vis_type_timelion/server/series_functions/multiply.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/multiply.test.js rename to src/plugins/vis_type_timelion/server/series_functions/multiply.test.js diff --git a/src/plugins/timelion/server/series_functions/points.js b/src/plugins/vis_type_timelion/server/series_functions/points.js similarity index 100% rename from src/plugins/timelion/server/series_functions/points.js rename to src/plugins/vis_type_timelion/server/series_functions/points.js diff --git a/src/plugins/timelion/server/series_functions/points.test.js b/src/plugins/vis_type_timelion/server/series_functions/points.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/points.test.js rename to src/plugins/vis_type_timelion/server/series_functions/points.test.js diff --git a/src/plugins/timelion/server/series_functions/precision.js b/src/plugins/vis_type_timelion/server/series_functions/precision.js similarity index 100% rename from src/plugins/timelion/server/series_functions/precision.js rename to src/plugins/vis_type_timelion/server/series_functions/precision.js diff --git a/src/plugins/timelion/server/series_functions/precision.test.js b/src/plugins/vis_type_timelion/server/series_functions/precision.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/precision.test.js rename to src/plugins/vis_type_timelion/server/series_functions/precision.test.js diff --git a/src/plugins/timelion/server/series_functions/props.js b/src/plugins/vis_type_timelion/server/series_functions/props.js similarity index 100% rename from src/plugins/timelion/server/series_functions/props.js rename to src/plugins/vis_type_timelion/server/series_functions/props.js diff --git a/src/plugins/timelion/server/series_functions/quandl.js b/src/plugins/vis_type_timelion/server/series_functions/quandl.js similarity index 100% rename from src/plugins/timelion/server/series_functions/quandl.js rename to src/plugins/vis_type_timelion/server/series_functions/quandl.js diff --git a/src/plugins/timelion/server/series_functions/quandl.test.js b/src/plugins/vis_type_timelion/server/series_functions/quandl.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/quandl.test.js rename to src/plugins/vis_type_timelion/server/series_functions/quandl.test.js diff --git a/src/plugins/timelion/server/series_functions/range.js b/src/plugins/vis_type_timelion/server/series_functions/range.js similarity index 100% rename from src/plugins/timelion/server/series_functions/range.js rename to src/plugins/vis_type_timelion/server/series_functions/range.js diff --git a/src/plugins/timelion/server/series_functions/range.test.js b/src/plugins/vis_type_timelion/server/series_functions/range.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/range.test.js rename to src/plugins/vis_type_timelion/server/series_functions/range.test.js diff --git a/src/plugins/timelion/server/series_functions/scale_interval.js b/src/plugins/vis_type_timelion/server/series_functions/scale_interval.js similarity index 100% rename from src/plugins/timelion/server/series_functions/scale_interval.js rename to src/plugins/vis_type_timelion/server/series_functions/scale_interval.js diff --git a/src/plugins/timelion/server/series_functions/scale_interval.test.js b/src/plugins/vis_type_timelion/server/series_functions/scale_interval.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/scale_interval.test.js rename to src/plugins/vis_type_timelion/server/series_functions/scale_interval.test.js diff --git a/src/plugins/timelion/server/series_functions/static.js b/src/plugins/vis_type_timelion/server/series_functions/static.js similarity index 100% rename from src/plugins/timelion/server/series_functions/static.js rename to src/plugins/vis_type_timelion/server/series_functions/static.js diff --git a/src/plugins/timelion/server/series_functions/static.test.js b/src/plugins/vis_type_timelion/server/series_functions/static.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/static.test.js rename to src/plugins/vis_type_timelion/server/series_functions/static.test.js diff --git a/src/plugins/timelion/server/series_functions/subtract.js b/src/plugins/vis_type_timelion/server/series_functions/subtract.js similarity index 100% rename from src/plugins/timelion/server/series_functions/subtract.js rename to src/plugins/vis_type_timelion/server/series_functions/subtract.js diff --git a/src/plugins/timelion/server/series_functions/subtract.test.js b/src/plugins/vis_type_timelion/server/series_functions/subtract.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/subtract.test.js rename to src/plugins/vis_type_timelion/server/series_functions/subtract.test.js diff --git a/src/plugins/timelion/server/series_functions/sum.js b/src/plugins/vis_type_timelion/server/series_functions/sum.js similarity index 100% rename from src/plugins/timelion/server/series_functions/sum.js rename to src/plugins/vis_type_timelion/server/series_functions/sum.js diff --git a/src/plugins/timelion/server/series_functions/sum.test.js b/src/plugins/vis_type_timelion/server/series_functions/sum.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/sum.test.js rename to src/plugins/vis_type_timelion/server/series_functions/sum.test.js diff --git a/src/plugins/timelion/server/series_functions/title.js b/src/plugins/vis_type_timelion/server/series_functions/title.js similarity index 100% rename from src/plugins/timelion/server/series_functions/title.js rename to src/plugins/vis_type_timelion/server/series_functions/title.js diff --git a/src/plugins/timelion/server/series_functions/title.test.js b/src/plugins/vis_type_timelion/server/series_functions/title.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/title.test.js rename to src/plugins/vis_type_timelion/server/series_functions/title.test.js diff --git a/src/plugins/timelion/server/series_functions/trend/index.js b/src/plugins/vis_type_timelion/server/series_functions/trend/index.js similarity index 100% rename from src/plugins/timelion/server/series_functions/trend/index.js rename to src/plugins/vis_type_timelion/server/series_functions/trend/index.js diff --git a/src/plugins/timelion/server/series_functions/trend/lib/regress.js b/src/plugins/vis_type_timelion/server/series_functions/trend/lib/regress.js similarity index 100% rename from src/plugins/timelion/server/series_functions/trend/lib/regress.js rename to src/plugins/vis_type_timelion/server/series_functions/trend/lib/regress.js diff --git a/src/plugins/timelion/server/series_functions/trim.js b/src/plugins/vis_type_timelion/server/series_functions/trim.js similarity index 100% rename from src/plugins/timelion/server/series_functions/trim.js rename to src/plugins/vis_type_timelion/server/series_functions/trim.js diff --git a/src/plugins/timelion/server/series_functions/trim.test.js b/src/plugins/vis_type_timelion/server/series_functions/trim.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/trim.test.js rename to src/plugins/vis_type_timelion/server/series_functions/trim.test.js diff --git a/src/plugins/timelion/server/series_functions/worldbank.js b/src/plugins/vis_type_timelion/server/series_functions/worldbank.js similarity index 100% rename from src/plugins/timelion/server/series_functions/worldbank.js rename to src/plugins/vis_type_timelion/server/series_functions/worldbank.js diff --git a/src/plugins/timelion/server/series_functions/worldbank_indicators.js b/src/plugins/vis_type_timelion/server/series_functions/worldbank_indicators.js similarity index 100% rename from src/plugins/timelion/server/series_functions/worldbank_indicators.js rename to src/plugins/vis_type_timelion/server/series_functions/worldbank_indicators.js diff --git a/src/plugins/timelion/server/series_functions/yaxis.js b/src/plugins/vis_type_timelion/server/series_functions/yaxis.js similarity index 100% rename from src/plugins/timelion/server/series_functions/yaxis.js rename to src/plugins/vis_type_timelion/server/series_functions/yaxis.js diff --git a/src/plugins/timelion/server/series_functions/yaxis.test.js b/src/plugins/vis_type_timelion/server/series_functions/yaxis.test.js similarity index 100% rename from src/plugins/timelion/server/series_functions/yaxis.test.js rename to src/plugins/vis_type_timelion/server/series_functions/yaxis.test.js diff --git a/src/plugins/timelion/server/timelion.json b/src/plugins/vis_type_timelion/server/timelion.json similarity index 100% rename from src/plugins/timelion/server/timelion.json rename to src/plugins/vis_type_timelion/server/timelion.json diff --git a/src/plugins/timelion/server/types.ts b/src/plugins/vis_type_timelion/server/types.ts similarity index 100% rename from src/plugins/timelion/server/types.ts rename to src/plugins/vis_type_timelion/server/types.ts diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json index 305e159ac2505..38662c6a7ff89 100644 --- a/src/plugins/vis_type_timeseries/kibana.json +++ b/src/plugins/vis_type_timeseries/kibana.json @@ -1,5 +1,5 @@ { - "id": "metrics", + "id": "visTypeTimeseries", "version": "8.0.0", "kibanaVersion": "kibana", "server": true, diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts b/src/plugins/vis_type_timeseries/server/config.ts similarity index 68% rename from src/legacy/core_plugins/kibana/public/visualize/legacy.ts rename to src/plugins/vis_type_timeseries/server/config.ts index 4ef2c93689714..f4668eff8fa04 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts +++ b/src/plugins/vis_type_timeseries/server/config.ts @@ -17,12 +17,15 @@ * under the License. */ -import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { plugin } from './index'; +import { schema, TypeOf } from '@kbn/config-schema'; -const instance = plugin({ - env: npSetup.plugins.kibanaLegacy.env, -} as PluginInitializerContext); -instance.setup(npSetup.core, npSetup.plugins); -instance.start(npStart.core, npStart.plugins); +export const config = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + + /** @deprecated **/ + chartResolution: schema.number({ defaultValue: 150 }), + /** @deprecated **/ + minimumBucketSize: schema.number({ defaultValue: 10 }), +}); + +export type VisTypeTimeseriesConfig = TypeOf; diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts index fa74b6e965971..f460257caf5e3 100644 --- a/src/plugins/vis_type_timeseries/server/index.ts +++ b/src/plugins/vis_type_timeseries/server/index.ts @@ -17,18 +17,25 @@ * under the License. */ -import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { VisTypeTimeseriesConfig, config as configSchema } from './config'; import { VisTypeTimeseriesPlugin } from './plugin'; + export { VisTypeTimeseriesSetup, Framework } from './plugin'; -export const config = { - schema: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - }), -}; +export const config: PluginConfigDescriptor = { + deprecations: ({ unused, renameFromRoot }) => [ + // In Kibana v7.8 plugin id was renamed from 'metrics' to 'vis_type_timeseries': + renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled', true), + renameFromRoot('metrics.chartResolution', 'vis_type_timeseries.chartResolution', true), + renameFromRoot('metrics.minimumBucketSize', 'vis_type_timeseries.minimumBucketSize', true), -export type VisTypeTimeseriesConfig = TypeOf; + // Unused properties which should be removed after releasing Kibana v8.0: + unused('chartResolution'), + unused('minimumBucketSize'), + ], + schema: configSchema, +}; export { ValidationTelemetryServiceSetup } from './validation_telemetry'; diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts index 6ef6362c6e37b..05257cb79a75c 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -29,7 +29,7 @@ import { } from 'src/core/server'; import { Observable } from 'rxjs'; import { Server } from 'hapi'; -import { VisTypeTimeseriesConfig } from '.'; +import { VisTypeTimeseriesConfig } from './config'; import { getVisData, GetVisData, GetVisDataOptions } from './lib/get_vis_data'; import { ValidationTelemetryService } from './validation_telemetry'; import { UsageCollectionSetup } from '../../usage_collection/server'; diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts index 77b49e824334f..f18fa1e4cc2fa 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts @@ -31,7 +31,7 @@ const resetCount: SavedObjectMigrationFn = doc => ({ export const tsvbTelemetrySavedObjectType: SavedObjectsType = { name: 'tsvb-validation-telemetry', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { failedRequests: { diff --git a/src/plugins/visualizations/public/index.scss b/src/plugins/visualizations/public/index.scss index eada763b63c4d..2b61535f3e7f2 100644 --- a/src/plugins/visualizations/public/index.scss +++ b/src/plugins/visualizations/public/index.scss @@ -1,2 +1,3 @@ @import 'wizard/index'; @import 'embeddable/index'; +@import 'components/index'; diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts index 9f4782f3ec730..cd2211c185530 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -23,7 +23,7 @@ import { visualizationSavedObjectTypeMigrations } from './visualization_migratio export const visualizationSavedObjectType: SavedObjectsType = { name: 'visualization', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', management: { icon: 'visualizeApp', defaultSearchField: 'title', diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json new file mode 100644 index 0000000000000..a7afa0697a5eb --- /dev/null +++ b/src/plugins/visualize/kibana.json @@ -0,0 +1,17 @@ +{ + "id": "visualize", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": [ + "data", + "kibanaLegacy", + "navigation", + "savedObjects", + "visualizations" + ], + "optionalPlugins": [ + "home", + "share" + ] +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/plugins/visualize/public/application/application.ts similarity index 95% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts rename to src/plugins/visualize/public/application/application.ts index 241397884c8fe..9d8a1b98ef023 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/plugins/visualize/public/application/application.ts @@ -17,16 +17,18 @@ * under the License. */ +import './index.scss'; + import angular, { IModule } from 'angular'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; -import { configureAppAngularModule } from '../legacy_imports'; -import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; +import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; import { + configureAppAngularModule, createTopNavDirective, createTopNavHelper, -} from '../../../../../../plugins/kibana_legacy/public'; +} from '../../../kibana_legacy/public'; // @ts-ignore import { initVisualizeApp } from './legacy_app'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/breadcrumbs.ts b/src/plugins/visualize/public/application/breadcrumbs.ts similarity index 86% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/breadcrumbs.ts rename to src/plugins/visualize/public/application/breadcrumbs.ts index b6a63d50b205b..972bdc1462b2c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/breadcrumbs.ts +++ b/src/plugins/visualize/public/application/breadcrumbs.ts @@ -24,7 +24,7 @@ import { VisualizeConstants } from './visualize_constants'; export function getLandingBreadcrumbs() { return [ { - text: i18n.translate('kbn.visualize.listing.breadcrumb', { + text: i18n.translate('visualize.listing.breadcrumb', { defaultMessage: 'Visualize', }), href: `#${VisualizeConstants.LANDING_PAGE_PATH}`, @@ -36,7 +36,7 @@ export function getWizardStep1Breadcrumbs() { return [ ...getLandingBreadcrumbs(), { - text: i18n.translate('kbn.visualize.wizard.step1Breadcrumb', { + text: i18n.translate('visualize.wizard.step1Breadcrumb', { defaultMessage: 'Create', }), }, @@ -47,7 +47,7 @@ export function getWizardStep2Breadcrumbs() { return [ ...getLandingBreadcrumbs(), { - text: i18n.translate('kbn.visualize.wizard.step2Breadcrumb', { + text: i18n.translate('visualize.wizard.step2Breadcrumb', { defaultMessage: 'Create', }), }, @@ -58,7 +58,7 @@ export function getCreateBreadcrumbs() { return [ ...getLandingBreadcrumbs(), { - text: i18n.translate('kbn.visualize.editor.createBreadcrumb', { + text: i18n.translate('visualize.editor.createBreadcrumb', { defaultMessage: 'Create', }), }, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss b/src/plugins/visualize/public/application/editor/_editor.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss rename to src/plugins/visualize/public/application/editor/_editor.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_index.scss b/src/plugins/visualize/public/application/editor/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_index.scss rename to src/plugins/visualize/public/application/editor/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html b/src/plugins/visualize/public/application/editor/editor.html similarity index 98% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html rename to src/plugins/visualize/public/application/editor/editor.html index 0dcacd30fba4e..a031d70ef9a83 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html +++ b/src/plugins/visualize/public/application/editor/editor.html @@ -80,7 +80,7 @@

) : null; @@ -229,10 +220,10 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState : []), { id: 'share', - label: i18n.translate('kbn.topNavMenu.shareVisualizationButtonLabel', { + label: i18n.translate('visualize.topNavMenu.shareVisualizationButtonLabel', { defaultMessage: 'share', }), - description: i18n.translate('kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel', { + description: i18n.translate('visualize.topNavMenu.shareVisualizationButtonAriaLabel', { defaultMessage: 'Share Visualization', }), testId: 'shareTopNavButton', @@ -252,13 +243,15 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState isDirty: hasUnappliedChanges || hasUnsavedChanges, }); }, + // disable the Share button if no action specified + disableButton: !share, }, { id: 'inspector', - label: i18n.translate('kbn.topNavMenu.openInspectorButtonLabel', { + label: i18n.translate('visualize.topNavMenu.openInspectorButtonLabel', { defaultMessage: 'inspect', }), - description: i18n.translate('kbn.visualize.topNavMenu.openInspectorButtonAriaLabel', { + description: i18n.translate('visualize.topNavMenu.openInspectorButtonAriaLabel', { defaultMessage: 'Open Inspector for visualization', }), testId: 'openInspectorButton', @@ -274,7 +267,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState }, tooltip() { if (!embeddableHandler.hasInspector || !embeddableHandler.hasInspector()) { - return i18n.translate('kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip', { + return i18n.translate('visualize.topNavMenu.openInspectorDisabledButtonTooltip', { defaultMessage: `This visualization doesn't support any inspectors.`, }); } @@ -319,7 +312,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState stopAllSyncing(); toastNotifications.addWarning({ - title: i18n.translate('kbn.visualize.visualizationTypeInvalidNotificationMessage', { + title: i18n.translate('visualize.visualizationTypeInvalidNotificationMessage', { defaultMessage: 'Invalid visualization type', }), text: toMountPoint({error.message}), @@ -359,7 +352,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState return; } - savedQueryService.getSavedQuery(savedQueryId).then(savedQuery => { + queryService.savedQueries.getSavedQuery(savedQueryId).then(savedQuery => { $scope.$evalAsync(() => { $scope.updateSavedQuery(savedQuery); }); @@ -622,7 +615,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState if (id) { toastNotifications.addSuccess({ title: i18n.translate( - 'kbn.visualize.topNavMenu.saveVisualization.successNotificationText', + 'visualize.topNavMenu.saveVisualization.successNotificationText', { defaultMessage: `Saved '{visTitle}'`, values: { @@ -661,15 +654,12 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState // eslint-disable-next-line console.error(error); toastNotifications.addDanger({ - title: i18n.translate( - 'kbn.visualize.topNavMenu.saveVisualization.failureNotificationText', - { - defaultMessage: `Error on saving '{visTitle}'`, - values: { - visTitle: savedVis.title, - }, - } - ), + title: i18n.translate('visualize.topNavMenu.saveVisualization.failureNotificationText', { + defaultMessage: `Error on saving '{visTitle}'`, + values: { + visTitle: savedVis.title, + }, + }), text: error.message, 'data-test-subj': 'saveVisualizationError', }); @@ -692,7 +682,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState }); toastNotifications.addSuccess( - i18n.translate('kbn.visualize.linkedToSearch.unlinkSuccessNotificationText', { + i18n.translate('visualize.linkedToSearch.unlinkSuccessNotificationText', { defaultMessage: `Unlinked from saved search '{searchTitle}'`, values: { searchTitle: savedSearch.title, @@ -704,7 +694,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState $scope.getAdditionalMessage = () => { return ( '' + - i18n.translate('kbn.visualize.experimentalVisInfoText', { + i18n.translate('visualize.experimentalVisInfoText', { defaultMessage: 'This visualization is marked as experimental.', }) + ' ' + diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts b/src/plugins/visualize/public/application/editor/lib/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts rename to src/plugins/visualize/public/application/editor/lib/index.ts diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts b/src/plugins/visualize/public/application/editor/lib/make_stateful.ts similarity index 91% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts rename to src/plugins/visualize/public/application/editor/lib/make_stateful.ts index 8384585108a59..d071df314d99c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts +++ b/src/plugins/visualize/public/application/editor/lib/make_stateful.ts @@ -17,8 +17,8 @@ * under the License. */ -import { PersistedState } from '../../../../../../../../plugins/visualizations/public'; -import { ReduxLikeStateContainer } from '../../../../../../../../plugins/kibana_utils/public'; +import { PersistedState } from '../../../../../visualizations/public'; +import { ReduxLikeStateContainer } from '../../../../../kibana_utils/public'; import { VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; /** diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts b/src/plugins/visualize/public/application/editor/lib/migrate_app_state.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts rename to src/plugins/visualize/public/application/editor/lib/migrate_app_state.ts diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts b/src/plugins/visualize/public/application/editor/lib/url_helper.test.ts similarity index 88% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts rename to src/plugins/visualize/public/application/editor/lib/url_helper.test.ts index e6974af9af832..09609e3d7e362 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts +++ b/src/plugins/visualize/public/application/editor/lib/url_helper.test.ts @@ -19,15 +19,6 @@ import { addEmbeddableToDashboardUrl } from './url_helper'; -jest.mock('../../../legacy_imports', () => ({ - DashboardConstants: { - ADD_EMBEDDABLE_ID: 'addEmbeddableId', - ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', - CREATE_NEW_DASHBOARD_URL: '/dashboard', - }, - VISUALIZE_EMBEDDABLE_TYPE: 'visualization', -})); - describe('', () => { it('addEmbeddableToDashboardUrl when dashboard is not saved', () => { const id = '123eb456cd'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts b/src/plugins/visualize/public/application/editor/lib/url_helper.ts similarity index 92% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts rename to src/plugins/visualize/public/application/editor/lib/url_helper.ts index c7937c856184a..84e1ef9687cd0 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts +++ b/src/plugins/visualize/public/application/editor/lib/url_helper.ts @@ -18,7 +18,8 @@ */ import { parseUrl, stringify } from 'query-string'; -import { DashboardConstants, VISUALIZE_EMBEDDABLE_TYPE } from '../../../legacy_imports'; +import { DashboardConstants } from '../../../../../dashboard/public'; +import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../visualizations/public'; /** * * Returns relative dashboard URL with added embeddableType and embeddableId query params diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts b/src/plugins/visualize/public/application/editor/lib/visualize_app_state.ts similarity index 98% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts rename to src/plugins/visualize/public/application/editor/lib/visualize_app_state.ts index 86f39ea76dd3a..fe2a19b7315c3 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts +++ b/src/plugins/visualize/public/application/editor/lib/visualize_app_state.ts @@ -24,7 +24,7 @@ import { createStateContainer, syncState, IKbnUrlStateStorage, -} from '../../../../../../../../plugins/kibana_utils/public'; +} from '../../../../../kibana_utils/public'; import { PureVisState, VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; const STATE_STORAGE_KEY = '_a'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/plugins/visualize/public/application/editor/visualization.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js rename to src/plugins/visualize/public/application/editor/visualization.js diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/plugins/visualize/public/application/editor/visualization_editor.js similarity index 98% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js rename to src/plugins/visualize/public/application/editor/visualization_editor.js index ef174dbaa5865..874b69532ec11 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/plugins/visualize/public/application/editor/visualization_editor.js @@ -44,7 +44,6 @@ export function initVisEditorDirective(app, deps) { editor.render({ core: deps.core, data: deps.data, - embeddable: deps.embeddable, uiState: $scope.uiState, timeRange: $scope.timeRange, filters: $scope.filters, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/help_menu/help_menu_util.js b/src/plugins/visualize/public/application/help_menu/help_menu_util.js similarity index 94% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/help_menu/help_menu_util.js rename to src/plugins/visualize/public/application/help_menu/help_menu_util.js index 9c00947d7663c..c297326f2e264 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/help_menu/help_menu_util.js +++ b/src/plugins/visualize/public/application/help_menu/help_menu_util.js @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; export function addHelpMenuToAppChrome(chrome, docLinks) { chrome.setHelpExtension({ - appName: i18n.translate('kbn.visualize.helpMenu.appName', { + appName: i18n.translate('visualize.helpMenu.appName', { defaultMessage: 'Visualize', }), links: [ diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/_index.scss b/src/plugins/visualize/public/application/index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/_index.scss rename to src/plugins/visualize/public/application/index.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/plugins/visualize/public/application/legacy_app.js similarity index 94% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js rename to src/plugins/visualize/public/application/legacy_app.js index a710d3e318749..7c5e3ce9408f0 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/plugins/visualize/public/application/legacy_app.js @@ -25,7 +25,8 @@ import { createKbnUrlStateStorage, redirectWhenMissing, ensureDefaultIndexPattern, -} from '../../../../../../plugins/kibana_utils/public'; +} from '../../../kibana_utils/public'; +import { createSavedSearchesLoader } from '../../../discover/public'; import editorTemplate from './editor/editor.html'; import visualizeListingTemplate from './listing/visualize_listing.html'; @@ -40,7 +41,6 @@ import { getCreateBreadcrumbs, getEditBreadcrumbs, } from './breadcrumbs'; -import { createSavedSearchesLoader } from '../../../../../../plugins/discover/public'; const getResolvedResults = deps => { const { core, data, visualizations, createVisEmbeddableFromObject } = deps; @@ -93,7 +93,7 @@ export function initVisualizeApp(app, deps) { app.factory('kbnUrlStateStorage', history => createKbnUrlStateStorage({ history, - useHash: deps.uiSettings.get('state:storeInSessionStorage'), + useHash: deps.core.uiSettings.get('state:storeInSessionStorage'), }) ); @@ -107,10 +107,10 @@ export function initVisualizeApp(app, deps) { } return { - text: i18n.translate('kbn.visualize.badge.readOnly.text', { + text: i18n.translate('visualize.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n.translate('kbn.visualize.badge.readOnly.tooltip', { + tooltip: i18n.translate('visualize.badge.readOnly.tooltip', { defaultMessage: 'Unable to save visualizations', }), iconType: 'glasses', @@ -156,7 +156,7 @@ export function initVisualizeApp(app, deps) { if (shouldHaveIndex && !hasIndex) { throw new Error( i18n.translate( - 'kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage', + 'visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage', { defaultMessage: 'You must provide either an indexPattern or a savedSearchId', } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/_index.scss b/src/plugins/visualize/public/application/listing/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/_index.scss rename to src/plugins/visualize/public/application/listing/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/_listing.scss b/src/plugins/visualize/public/application/listing/_listing.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/_listing.scss rename to src/plugins/visualize/public/application/listing/_listing.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.html b/src/plugins/visualize/public/application/listing/visualize_listing.html similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.html rename to src/plugins/visualize/public/application/listing/visualize_listing.html diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js b/src/plugins/visualize/public/application/listing/visualize_listing.js similarity index 93% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js rename to src/plugins/visualize/public/application/listing/visualize_listing.js index 098633d046062..900c17fa394de 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js +++ b/src/plugins/visualize/public/application/listing/visualize_listing.js @@ -24,7 +24,7 @@ import { VisualizeConstants } from '../visualize_constants'; import { i18n } from '@kbn/i18n'; import { getServices } from '../../kibana_services'; -import { syncQueryStateWithUrl } from '../../../../../../../plugins/data/public'; +import { syncQueryStateWithUrl } from '../../../../data/public'; export function initListingDirective(app, I18nContext) { app.directive('visualizeListingTable', reactDirective => @@ -40,9 +40,8 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor savedVisualizations, data: { query }, toastNotifications, - uiSettings, visualizations, - core: { docLinks, savedObjects }, + core: { docLinks, savedObjects, uiSettings }, } = getServices(); // syncs `_g` portion of url with query services @@ -110,7 +109,7 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor }) ).catch(error => { toastNotifications.addError(error, { - title: i18n.translate('kbn.visualize.visualizeListingDeleteErrorTitle', { + title: i18n.translate('visualize.visualizeListingDeleteErrorTitle', { defaultMessage: 'Error deleting visualization', }), }); @@ -119,7 +118,7 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor chrome.setBreadcrumbs([ { - text: i18n.translate('kbn.visualize.visualizeListingBreadcrumbsTitle', { + text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { defaultMessage: 'Visualize', }), }, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js b/src/plugins/visualize/public/application/listing/visualize_listing_table.js similarity index 83% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js rename to src/plugins/visualize/public/application/listing/visualize_listing_table.js index 932ac8996e97e..100becdd3a80f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js +++ b/src/plugins/visualize/public/application/listing/visualize_listing_table.js @@ -21,7 +21,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { TableListView } from '../../../../../../../plugins/kibana_react/public'; +import { TableListView } from '../../../../kibana_react/public'; import { EuiIcon, EuiBetaBadge, EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; @@ -33,7 +33,7 @@ class VisualizeListingTable extends Component { } render() { - const { visualizeCapabilities, uiSettings, toastNotifications } = getServices(); + const { visualizeCapabilities, core, toastNotifications } = getServices(); return ( item.canDelete} initialFilter={''} noItemsFragment={this.getNoItemsMessage()} - entityName={i18n.translate('kbn.visualize.listing.table.entityName', { + entityName={i18n.translate('visualize.listing.table.entityName', { defaultMessage: 'visualization', })} - entityNamePlural={i18n.translate('kbn.visualize.listing.table.entityNamePlural', { + entityNamePlural={i18n.translate('visualize.listing.table.entityNamePlural', { defaultMessage: 'visualizations', })} - tableListTitle={i18n.translate('kbn.visualize.listing.table.listTitle', { + tableListTitle={i18n.translate('visualize.listing.table.listTitle', { defaultMessage: 'Visualizations', })} toastNotifications={toastNotifications} - uiSettings={uiSettings} + uiSettings={core.uiSettings} /> ); } @@ -67,7 +67,7 @@ class VisualizeListingTable extends Component { const tableColumns = [ { field: 'title', - name: i18n.translate('kbn.visualize.listing.table.titleColumnName', { + name: i18n.translate('visualize.listing.table.titleColumnName', { defaultMessage: 'Title', }), sortable: true, @@ -82,7 +82,7 @@ class VisualizeListingTable extends Component { }, { field: 'typeTitle', - name: i18n.translate('kbn.visualize.listing.table.typeColumnName', { + name: i18n.translate('visualize.listing.table.typeColumnName', { defaultMessage: 'Type', }), sortable: true, @@ -96,7 +96,7 @@ class VisualizeListingTable extends Component { }, { field: 'description', - name: i18n.translate('kbn.dashboard.listing.table.descriptionColumnName', { + name: i18n.translate('visualize.listing.table.descriptionColumnName', { defaultMessage: 'Description', }), sortable: true, @@ -116,7 +116,7 @@ class VisualizeListingTable extends Component { title={

@@ -133,7 +133,7 @@ class VisualizeListingTable extends Component { title={

@@ -142,7 +142,7 @@ class VisualizeListingTable extends Component {

@@ -156,7 +156,7 @@ class VisualizeListingTable extends Component { data-test-subj="createVisualizationPromptButton" > @@ -192,10 +192,10 @@ class VisualizeListingTable extends Component { { return new VisualizePlugin(context); }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/plugins/visualize/public/kibana_services.ts similarity index 68% rename from src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts rename to src/plugins/visualize/public/kibana_services.ts index d5440c4677d8a..765e9a82ae899 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/plugins/visualize/public/kibana_services.ts @@ -22,21 +22,18 @@ import { CoreStart, SavedObjectsClientContract, ToastsStart, - IUiSettingsClient, - I18nStart, PluginInitializerContext, + I18nStart, } from 'kibana/public'; -import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; -import { Storage } from '../../../../../plugins/kibana_utils/public'; -import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; -import { SharePluginStart } from '../../../../../plugins/share/public'; -import { DataPublicPluginStart, IndexPatternsContract } from '../../../../../plugins/data/public'; -import { VisualizationsStart } from '../../../../../plugins/visualizations/public'; -import { SavedVisualizations } from './np_ready/types'; -import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; -import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; -import { DefaultEditorController } from '../../../../../plugins/vis_default_editor/public'; +import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; +import { Storage } from '../../kibana_utils/public'; +import { SharePluginStart } from '../../share/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { VisualizationsStart } from '../../visualizations/public'; +import { SavedVisualizations } from './application/types'; +import { KibanaLegacyStart } from '../../kibana_legacy/public'; +import { DefaultEditorController } from '../../vis_default_editor/public'; export interface VisualizeKibanaServices { pluginInitializerContext: PluginInitializerContext; @@ -44,20 +41,15 @@ export interface VisualizeKibanaServices { chrome: ChromeStart; core: CoreStart; data: DataPublicPluginStart; - embeddable: EmbeddableStart; - indexPatterns: IndexPatternsContract; localStorage: Storage; navigation: NavigationStart; toastNotifications: ToastsStart; savedObjectsClient: SavedObjectsClientContract; - savedQueryService: DataPublicPluginStart['query']['savedQueries']; savedVisualizations: SavedVisualizations; - share: SharePluginStart; - uiSettings: IUiSettingsClient; + share?: SharePluginStart; config: KibanaLegacyStart['config']; visualizeCapabilities: any; visualizations: VisualizationsStart; - usageCollection?: UsageCollectionSetup; I18nContext: I18nStart['Context']; setActiveUrl: (newUrl: string) => void; DefaultVisualizationEditor: typeof DefaultEditorController; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/plugins/visualize/public/plugin.ts similarity index 53% rename from src/legacy/core_plugins/kibana/public/visualize/plugin.ts rename to src/plugins/visualize/public/plugin.ts index 4ffbc307c69a8..ab64e083a553d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -27,64 +27,43 @@ import { CoreStart, Plugin, PluginInitializerContext, - SavedObjectsClientContract, } from 'kibana/public'; -import { Storage, createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public'; -import { - DataPublicPluginStart, - DataPublicPluginSetup, - esFilters, -} from '../../../../../plugins/data/public'; -import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; -import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; -import { SharePluginStart } from '../../../../../plugins/share/public'; -import { - KibanaLegacySetup, - AngularRenderedAppUpdater, -} from '../../../../../plugins/kibana_legacy/public'; -import { VisualizationsStart } from '../../../../../plugins/visualizations/public'; -import { VisualizeConstants } from './np_ready/visualize_constants'; +import { Storage, createKbnUrlTracker } from '../../kibana_utils/public'; +import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; +import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; +import { SharePluginStart } from '../../share/public'; +import { KibanaLegacySetup, AngularRenderedAppUpdater } from '../../kibana_legacy/public'; +import { VisualizationsStart } from '../../visualizations/public'; +import { VisualizeConstants } from './application/visualize_constants'; import { setServices, VisualizeKibanaServices } from './kibana_services'; -import { - FeatureCatalogueCategory, - HomePublicPluginSetup, -} from '../../../../../plugins/home/public'; -import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; -import { DefaultEditorController } from '../../../../../plugins/vis_default_editor/public'; +import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; +import { DefaultEditorController } from '../../vis_default_editor/public'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; - embeddable: EmbeddableStart; navigation: NavigationStart; - share: SharePluginStart; + share?: SharePluginStart; visualizations: VisualizationsStart; } export interface VisualizePluginSetupDependencies { - home: HomePublicPluginSetup; + home?: HomePublicPluginSetup; kibanaLegacy: KibanaLegacySetup; - usageCollection?: UsageCollectionSetup; data: DataPublicPluginSetup; } -export class VisualizePlugin implements Plugin { - private startDependencies: { - data: DataPublicPluginStart; - embeddable: EmbeddableStart; - navigation: NavigationStart; - savedObjectsClient: SavedObjectsClientContract; - share: SharePluginStart; - visualizations: VisualizationsStart; - } | null = null; +export class VisualizePlugin + implements + Plugin { private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; constructor(private initializerContext: PluginInitializerContext) {} public async setup( - core: CoreSetup, - { home, kibanaLegacy, usageCollection, data }: VisualizePluginSetupDependencies + core: CoreSetup, + { home, kibanaLegacy, data }: VisualizePluginSetupDependencies ) { const { appMounted, appUnMounted, stop: stopUrlTracker, setActiveUrl } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), @@ -117,50 +96,34 @@ export class VisualizePlugin implements Plugin { updater$: this.appStateUpdater.asObservable(), navLinkId: 'kibana:visualize', mount: async (params: AppMountParameters) => { - const [coreStart] = await core.getStartServices(); - - if (this.startDependencies === null) { - throw new Error('not started yet'); - } + const [coreStart, pluginsStart] = await core.getStartServices(); appMounted(); - const { - savedObjectsClient, - embeddable, - navigation, - visualizations, - data: dataStart, - share, - } = this.startDependencies; const deps: VisualizeKibanaServices = { pluginInitializerContext: this.initializerContext, addBasePath: coreStart.http.basePath.prepend, core: coreStart, + config: kibanaLegacy.config, chrome: coreStart.chrome, - data: dataStart, - embeddable, - indexPatterns: dataStart.indexPatterns, + data: pluginsStart.data, localStorage: new Storage(localStorage), - navigation, - savedObjectsClient, - savedVisualizations: visualizations.savedVisualizationsLoader, - savedQueryService: dataStart.query.savedQueries, - share, + navigation: pluginsStart.navigation, + savedObjectsClient: coreStart.savedObjects.client, + savedVisualizations: pluginsStart.visualizations.savedVisualizationsLoader, + share: pluginsStart.share, toastNotifications: coreStart.notifications.toasts, - uiSettings: coreStart.uiSettings, - config: kibanaLegacy.config, visualizeCapabilities: coreStart.application.capabilities.visualize, - visualizations, - usageCollection, + visualizations: pluginsStart.visualizations, I18nContext: coreStart.i18n.Context, setActiveUrl, DefaultVisualizationEditor: DefaultEditorController, - createVisEmbeddableFromObject: visualizations.__LEGACY.createVisEmbeddableFromObject, + createVisEmbeddableFromObject: + pluginsStart.visualizations.__LEGACY.createVisEmbeddableFromObject, }; setServices(deps); - const { renderApp } = await import('./np_ready/application'); + const { renderApp } = await import('./application/application'); const unmount = renderApp(params.element, params.appBasePath, deps); return () => { unmount(); @@ -169,33 +132,23 @@ export class VisualizePlugin implements Plugin { }, }); - home.featureCatalogue.register({ - id: 'visualize', - title: 'Visualize', - description: i18n.translate('kbn.visualize.visualizeDescription', { - defaultMessage: - 'Create visualizations and aggregate data stores in your Elasticsearch indices.', - }), - icon: 'visualizeApp', - path: `/app/kibana#${VisualizeConstants.LANDING_PAGE_PATH}`, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, - }); + if (home) { + home.featureCatalogue.register({ + id: 'visualize', + title: 'Visualize', + description: i18n.translate('visualize.visualizeDescription', { + defaultMessage: + 'Create visualizations and aggregate data stores in your Elasticsearch indices.', + }), + icon: 'visualizeApp', + path: `/app/kibana#${VisualizeConstants.LANDING_PAGE_PATH}`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + } } - public start( - core: CoreStart, - { embeddable, navigation, data, share, visualizations }: VisualizePluginStartDependencies - ) { - this.startDependencies = { - data, - embeddable, - navigation, - savedObjectsClient: core.savedObjects.client, - share, - visualizations, - }; - } + public start(core: CoreStart, plugins: VisualizePluginStartDependencies) {} stop() { if (this.stopUrlTracking) { diff --git a/tasks/config/peg.js b/tasks/config/peg.js index 1d8667840faba..2de6ff4b5cff9 100644 --- a/tasks/config/peg.js +++ b/tasks/config/peg.js @@ -26,7 +26,7 @@ module.exports = { }, }, timelion_chain: { - src: 'src/legacy/core_plugins/vis_type_timelion/public/chain.peg', - dest: 'src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js', + src: 'src/plugins/vis_type_timelion/public/chain.peg', + dest: 'src/plugins/vis_type_timelion/public/_generated_/chain.js', }, }; diff --git a/test/api_integration/apis/saved_objects/migrations.js b/test/api_integration/apis/saved_objects/migrations.js index bd207ccb41b20..9d908b95ca575 100644 --- a/test/api_integration/apis/saved_objects/migrations.js +++ b/test/api_integration/apis/saved_objects/migrations.js @@ -383,7 +383,7 @@ function migrationsToTypes(migrations) { return Object.entries(migrations).map(([type, migrations]) => ({ name: type, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: {} }, migrations: { ...migrations }, })); diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.js b/test/functional/apps/dashboard/create_and_add_embeddables.js index 3ce8e353e61fc..410acdcb5680d 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.js +++ b/test/functional/apps/dashboard/create_and_add_embeddables.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; -import { VisualizeConstants } from '../../../../src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants'; +import { VisualizeConstants } from '../../../../src/plugins/visualize/public/application/visualize_constants'; export default function({ getService, getPageObjects }) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js index f30f58913bd97..52c4a11360355 100644 --- a/test/functional/apps/dashboard/panel_controls.js +++ b/test/functional/apps/dashboard/panel_controls.js @@ -24,7 +24,7 @@ import { AREA_CHART_VIS_NAME, LINE_CHART_VIS_NAME, } from '../../page_objects/dashboard_page'; -import { VisualizeConstants } from '../../../../src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants'; +import { VisualizeConstants } from '../../../../src/plugins/visualize/public/application/visualize_constants'; export default function({ getService, getPageObjects }) { const browser = getService('browser'); diff --git a/test/functional/config.edge.js b/test/functional/config.edge.js new file mode 100644 index 0000000000000..ed68b41e8c89a --- /dev/null +++ b/test/functional/config.edge.js @@ -0,0 +1,34 @@ +/* + * 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 default async function({ readConfigFile }) { + const defaultConfig = await readConfigFile(require.resolve('./config')); + + return { + ...defaultConfig.getAll(), + + browser: { + type: 'msedge', + }, + + junit: { + reportName: 'MS Chromium Edge UI Functional Tests', + }, + }; +} diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 3b63fa68d71ee..220c2d8f6b363 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -18,7 +18,7 @@ */ import { FtrProviderContext } from '../ftr_provider_context'; -import { VisualizeConstants } from '../../../src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants'; +import { VisualizeConstants } from '../../../src/plugins/visualize/public/application/visualize_constants'; export function VisualizePageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 5017947e95d03..13d2365c07191 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -47,7 +47,9 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { */ public readonly browserType: string = browserType; - public readonly isChrome: boolean = browserType === Browsers.Chrome; + public readonly isChromium: boolean = [Browsers.Chrome, Browsers.ChromiumEdge].includes( + browserType + ); public readonly isFirefox: boolean = browserType === Browsers.Firefox; diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 157918df874c8..8b57ecd3c8235 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -55,6 +55,7 @@ export class WebElementWrapper { private driver: WebDriver = this.webDriver.driver; private Keys = Key; public isW3CEnabled: boolean = (this.webDriver.driver as any).executor_.w3c === true; + public isChromium: boolean = [Browsers.Chrome, Browsers.ChromiumEdge].includes(this.browserType); public static create( webElement: WebElement | WebElementWrapper, @@ -63,7 +64,7 @@ export class WebElementWrapper { timeout: number, fixedHeaderHeight: number, logger: ToolingLog, - browserType: string + browserType: Browsers ): WebElementWrapper { if (webElement instanceof WebElementWrapper) { return webElement; @@ -87,7 +88,7 @@ export class WebElementWrapper { private timeout: number, private fixedHeaderHeight: number, private logger: ToolingLog, - private browserType: string + private browserType: Browsers ) {} private async _findWithCustomTimeout( @@ -243,7 +244,7 @@ export class WebElementWrapper { return this.clearValueWithKeyboard(); } await this.retryCall(async function clearValue(wrapper) { - if (wrapper.browserType === Browsers.Chrome || options.withJS) { + if (wrapper.isChromium || options.withJS) { // https://bugs.chromium.org/p/chromedriver/issues/detail?id=2702 await wrapper.driver.executeScript(`arguments[0].value=''`, wrapper._webElement); } else { @@ -275,7 +276,7 @@ export class WebElementWrapper { await delay(100); } } else { - if (this.browserType === Browsers.Chrome) { + if (this.isChromium) { // https://bugs.chromium.org/p/chromedriver/issues/detail?id=30 await this.retryCall(async function clearValueWithKeyboard(wrapper) { await wrapper.driver.executeScript(`arguments[0].select();`, wrapper._webElement); diff --git a/test/functional/services/remote/browsers.ts b/test/functional/services/remote/browsers.ts index 46d81f1737a55..aa6e364d0a09d 100644 --- a/test/functional/services/remote/browsers.ts +++ b/test/functional/services/remote/browsers.ts @@ -21,4 +21,5 @@ export enum Browsers { Chrome = 'chrome', Firefox = 'firefox', InternetExplorer = 'ie', + ChromiumEdge = 'msedge', } diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index e571a1a7e5551..933b08f7681e8 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -69,13 +69,15 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { const caps = await driver.getCapabilities(); const browserVersion = caps.get(isW3CEnabled ? 'browserVersion' : 'version'); - log.info(`Remote initialized: ${caps.get('browserName')} ${browserVersion}`); + log.info( + `Remote initialized: ${caps.get( + 'browserName' + )} ${browserVersion}, w3c compliance=${isW3CEnabled}, collectingCoverage=${collectCoverage}` + ); - if (browserType === Browsers.Chrome) { + if ([Browsers.Chrome, Browsers.ChromiumEdge].includes(browserType)) { log.info( - `Chromedriver version: ${ - caps.get('chrome').chromedriverVersion - }, w3c=${isW3CEnabled}, codeCoverage=${collectCoverage}` + `${browserType}driver version: ${caps.get(browserType)[`${browserType}driverVersion`]}` ); } diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 3bf5b865aa7ba..1b7ef2c1855d0 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -30,10 +30,12 @@ import geckoDriver from 'geckodriver'; import { Builder, Capabilities, By, logging, until } from 'selenium-webdriver'; import chrome from 'selenium-webdriver/chrome'; import firefox from 'selenium-webdriver/firefox'; +import edge from 'selenium-webdriver/edge'; // @ts-ignore internal modules are not typed import { Executor } from 'selenium-webdriver/lib/http'; // @ts-ignore internal modules are not typed import { getLogger } from 'selenium-webdriver/lib/logging'; +import { installDriver } from 'ms-chromium-edge-driver'; import { pollForLogEntry$ } from './poll_for_log_entry'; import { createStdoutSocket } from './create_stdout_stream'; @@ -63,6 +65,7 @@ Executor.prototype.execute = preventParallelCalls( ); let attemptCounter = 0; +let edgePaths: { driverPath: string | undefined; browserPath: string | undefined }; async function attemptToCreateCommand( log: ToolingLog, browserType: Browsers, @@ -136,6 +139,46 @@ async function attemptToCreateCommand( }; } + case 'msedge': { + if (edgePaths && edgePaths.browserPath && edgePaths.driverPath) { + const edgeOptions = new edge.Options(); + if (headlessBrowser === '1') { + // @ts-ignore internal modules are not typed + edgeOptions.headless(); + } + // @ts-ignore internal modules are not typed + edgeOptions.setEdgeChromium(true); + // @ts-ignore internal modules are not typed + edgeOptions.setBinaryPath(edgePaths.browserPath); + const session = await new Builder() + .forBrowser('MicrosoftEdge') + .setEdgeOptions(edgeOptions) + .setEdgeService(new edge.ServiceBuilder(edgePaths.driverPath)) + .build(); + return { + session, + consoleLog$: pollForLogEntry$( + session, + logging.Type.BROWSER, + logPollingMs, + lifecycle.cleanup.after$ + ).pipe( + takeUntil(lifecycle.cleanup.after$), + map(({ message, level: { name: level } }) => ({ + message: message.replace(/\\n/g, '\n'), + level, + })) + ), + }; + } else { + throw new Error( + `Chromium Edge session requires browser or driver path to be defined: ${JSON.stringify( + edgePaths + )}` + ); + } + } + case 'firefox': { const firefoxOptions = new firefox.Options(); // Firefox 65+ supports logging console output to stdout @@ -265,6 +308,11 @@ export async function initWebDriver( log.verbose(entry.message); }); + // download Edge driver only in case of usage + if (browserType === Browsers.ChromiumEdge) { + edgePaths = await installDriver(); + } + return await Promise.race([ (async () => { await delay(2 * MINUTE); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json index d8096d9aab27a..544c27241f5cb 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/tsconfig.json @@ -6,7 +6,8 @@ "types": [ "node", "jest", - "react" + "react", + "flot" ] }, "include": [ @@ -16,4 +17,4 @@ "../../../../typings/**/*", ], "exclude": [] -} \ No newline at end of file +} diff --git a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts index 3f6a8e8773e04..f0b1cde24c6fe 100644 --- a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts +++ b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts @@ -17,15 +17,13 @@ * under the License. */ -import { Plugin, CoreSetup, IRenderOptions } from 'kibana/server'; +import { Plugin, CoreSetup } from 'kibana/server'; import { schema } from '@kbn/config-schema'; export class RenderingPlugin implements Plugin { public setup(core: CoreSetup) { - const router = core.http.createRouter(); - - router.get( + core.http.resources.register( { path: '/render/{id}', validate: { @@ -41,18 +39,12 @@ export class RenderingPlugin implements Plugin { }, }, async (context, req, res) => { - const { id } = req.params; const { includeUserSettings } = req.query; - const app = { getId: () => id! }; - const options: Partial = { app, includeUserSettings }; - const body = await context.core.rendering.render(options); - return res.ok({ - body, - headers: { - 'content-security-policy': core.http.csp.header, - }, - }); + if (includeUserSettings) { + return res.renderCoreApp(); + } + return res.renderAnonymousCoreApp(); } ); } diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 23807a6e98dc2..c77ffe40db553 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -22,7 +22,7 @@ else echo " -> running tests from the clone folder" #yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; - node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --config test/functional/config.coverage.js || true; + node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --exclude-tag "skipCoverage" || true; if [[ -d target/kibana-coverage/functional ]]; then echo " -> replacing kibana${CI_GROUP} with kibana in json files" diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 5055997df642a..7070fb1063cb5 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -39,7 +39,7 @@ else # build runtime for canvas echo "NODE_ENV=$NODE_ENV" node ./legacy/plugins/canvas/scripts/shareable_runtime - node scripts/jest --ci --verbose --coverage + node --max-old-space-size=6144 scripts/jest --ci --verbose --coverage # rename file in order to be unique one test -f ../target/kibana-coverage/jest/coverage-final.json \ && mv ../target/kibana-coverage/jest/coverage-final.json \ diff --git a/test/scripts/jenkins_xpack_ci_group.sh b/test/scripts/jenkins_xpack_ci_group.sh index 01b13293c10ba..a6e600630364e 100755 --- a/test/scripts/jenkins_xpack_ci_group.sh +++ b/test/scripts/jenkins_xpack_ci_group.sh @@ -23,7 +23,7 @@ else cd "kibana${CI_GROUP}/x-pack" echo " -> running tests from the clone folder" - node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --config test/functional/config.coverage.js || true; + node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --exclude-tag "skipCoverage" || true; if [[ -d ../target/kibana-coverage/functional ]]; then echo " -> replacing kibana${CI_GROUP} with kibana in json files" diff --git a/test/tsconfig.json b/test/tsconfig.json index 285d3db64a874..5a3716e620fed 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "types": [ "node", - "mocha" + "mocha", + "flot" ], "lib": [ "esnext", diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx index a1462c7637358..cc5c62e25b491 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx @@ -57,7 +57,8 @@ export class MachineLearningFlyout extends Component { }; public addErrorToast = () => { - const core = this.context; + const { core } = this.context; + const { urlParams } = this.props; const { serviceName } = urlParams; diff --git a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts b/x-pack/legacy/plugins/apm/public/services/rest/ml.ts index 1c618098b36e3..0cd1bdf907531 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/ml.ts @@ -12,7 +12,8 @@ import { } from '../../../../../../plugins/apm/common/elasticsearch_fieldnames'; import { getMlJobId, - getMlPrefix + getMlPrefix, + encodeForMlApi } from '../../../../../../plugins/apm/common/ml_job_constants'; import { callApi } from './callApi'; import { ESFilter } from '../../../../../../plugins/apm/typings/elasticsearch'; @@ -53,13 +54,16 @@ export async function startMLJob({ http: HttpSetup; }) { const transactionIndices = await getTransactionIndices(http); - const groups = ['apm', serviceName.toLowerCase()]; + const groups = [ + 'apm', + encodeForMlApi(serviceName), + encodeForMlApi(transactionType) + ]; const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } } ]; - groups.push(transactionType.toLowerCase()); return callApi(http, { method: 'POST', pathname: `/api/ml/modules/setup/apm_transaction`, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js index b50c36aa8df9f..24bc7e17356e2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js @@ -145,9 +145,35 @@ export const updateFollowerIndex = (id, followerIndex) => { if (isUsingAdvancedSettings) { uiMetrics.push(UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS); } + + const { + maxReadRequestOperationCount, + maxOutstandingReadRequests, + maxReadRequestSize, + maxWriteRequestOperationCount, + maxWriteRequestSize, + maxOutstandingWriteRequests, + maxWriteBufferCount, + maxWriteBufferSize, + maxRetryDelay, + readPollTimeout, + } = followerIndex; + const request = httpClient.put(`${API_BASE_PATH}/follower_indices/${encodeURIComponent(id)}`, { - body: JSON.stringify(followerIndex), + body: JSON.stringify({ + maxReadRequestOperationCount, + maxOutstandingReadRequests, + maxReadRequestSize, + maxWriteRequestOperationCount, + maxWriteRequestSize, + maxOutstandingWriteRequests, + maxWriteBufferCount, + maxWriteBufferSize, + maxRetryDelay, + readPollTimeout, + }), }); + return trackUserRequest(request, uiMetrics); }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts index 3896e1c02c915..1d7dacf4a8688 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts @@ -164,6 +164,18 @@ export const registerFollowerIndexRoutes = ({ router, __LEGACY }: RouteDependenc path: `${API_BASE_PATH}/follower_indices/{id}`, validate: { params: schema.object({ id: schema.string() }), + body: schema.object({ + maxReadRequestOperationCount: schema.maybe(schema.number()), + maxOutstandingReadRequests: schema.maybe(schema.number()), + maxReadRequestSize: schema.maybe(schema.string()), // byte value + maxWriteRequestOperationCount: schema.maybe(schema.number()), + maxWriteRequestSize: schema.maybe(schema.string()), // byte value + maxOutstandingWriteRequests: schema.maybe(schema.number()), + maxWriteBufferCount: schema.maybe(schema.number()), + maxWriteBufferSize: schema.maybe(schema.string()), // byte value + maxRetryDelay: schema.maybe(schema.string()), // time value + readPollTimeout: schema.maybe(schema.string()), // time value + }), }, }, licensePreRoutingFactory({ diff --git a/x-pack/legacy/plugins/maps/common/get_join_key.ts b/x-pack/legacy/plugins/maps/common/get_join_key.ts new file mode 100644 index 0000000000000..004f12ca08d2e --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/get_join_key.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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export * from '../../../../plugins/maps/common/get_join_key'; diff --git a/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.test.ts b/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.test.ts new file mode 100644 index 0000000000000..d92bf06541433 --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { LAYER_TYPE } from '../constants'; +import { migrateJoinAggKey } from './join_agg_key'; + +describe('migrateJoinAggKey', () => { + const joins = [ + { + leftField: 'machine.os', + right: { + id: '9055b4aa-136a-4b6d-90ab-9f94ccfe5eb5', + indexPatternTitle: 'kibana_sample_data_logs', + term: 'machine.os.keyword', + metrics: [ + { + type: 'avg', + field: 'bytes', + }, + { + type: 'count', + }, + ], + whereQuery: { + query: 'bytes > 10000', + language: 'kuery', + }, + indexPatternRefName: 'layer_1_join_0_index_pattern', + }, + }, + { + leftField: 'machine.os', + right: { + id: '9a7f4e71-9500-4512-82f1-b7eaee3d87ff', + indexPatternTitle: 'kibana_sample_data_logs', + term: 'machine.os.keyword', + whereQuery: { + query: 'bytes < 10000', + language: 'kuery', + }, + metrics: [ + { + type: 'avg', + field: 'bytes', + }, + ], + indexPatternRefName: 'layer_1_join_1_index_pattern', + }, + }, + ]; + + test('Should handle missing layerListJSON attribute', () => { + const attributes = { + title: 'my map', + }; + expect(migrateJoinAggKey({ attributes })).toEqual({ + title: 'my map', + }); + }); + + test('Should migrate vector styles from legacy join agg key to new join agg key', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + joins, + style: { + properties: { + fillColor: { + type: 'DYNAMIC', + options: { + color: 'Blues', + colorCategory: 'palette_0', + field: { + name: + '__kbnjoin__avg_of_bytes_groupby_kibana_sample_data_logs.machine.os.keyword', + origin: 'join', + }, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + type: 'ORDINAL', + }, + }, + lineColor: { + type: 'DYNAMIC', + options: { + color: 'Blues', + colorCategory: 'palette_0', + field: { + name: '__kbnjoin__count_groupby_kibana_sample_data_logs.machine.os.keyword', + origin: 'join', + }, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + type: 'ORDINAL', + }, + }, + lineWidth: { + type: 'DYNAMIC', + options: { + color: 'Blues', + colorCategory: 'palette_0', + field: { + name: 'mySourceField', + origin: 'source', + }, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + type: 'ORDINAL', + }, + }, + }, + }, + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + const { layerListJSON: migratedLayerListJSON } = migrateJoinAggKey({ attributes }); + const migratedLayerList = JSON.parse(migratedLayerListJSON!); + expect(migratedLayerList[0].style.properties.fillColor.options.field.name).toBe( + '__kbnjoin__avg_of_bytes__9055b4aa-136a-4b6d-90ab-9f94ccfe5eb5' + ); + expect(migratedLayerList[0].style.properties.lineColor.options.field.name).toBe( + '__kbnjoin__count__9055b4aa-136a-4b6d-90ab-9f94ccfe5eb5' + ); + expect(migratedLayerList[0].style.properties.lineWidth.options.field.name).toBe( + 'mySourceField' + ); + }); +}); diff --git a/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts b/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts new file mode 100644 index 0000000000000..29661aedb550c --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts @@ -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 _ from 'lodash'; +import { + AGG_DELIMITER, + AGG_TYPE, + FIELD_ORIGIN, + JOIN_FIELD_NAME_PREFIX, + LAYER_TYPE, + VECTOR_STYLES, +} from '../constants'; +import { getJoinAggKey } from '../get_join_key'; +import { + AggDescriptor, + JoinDescriptor, + LayerDescriptor, + VectorLayerDescriptor, +} from '../descriptor_types'; +import { MapSavedObjectAttributes } from '../../../../../plugins/maps/common/map_saved_object_type'; + +const GROUP_BY_DELIMITER = '_groupby_'; + +function getLegacyAggKey({ + aggType, + aggFieldName, + indexPatternTitle, + termFieldName, +}: { + aggType: AGG_TYPE; + aggFieldName?: string; + indexPatternTitle: string; + termFieldName: string; +}): string { + const metricKey = + aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${aggFieldName}` : aggType; + return `${JOIN_FIELD_NAME_PREFIX}${metricKey}${GROUP_BY_DELIMITER}${indexPatternTitle}.${termFieldName}`; +} + +function parseLegacyAggKey(legacyAggKey: string): { aggType: AGG_TYPE; aggFieldName?: string } { + const groupBySplit = legacyAggKey + .substring(JOIN_FIELD_NAME_PREFIX.length) + .split(GROUP_BY_DELIMITER); + const metricKey = groupBySplit[0]; + const metricKeySplit = metricKey.split(AGG_DELIMITER); + return { + aggType: metricKeySplit[0] as AGG_TYPE, + aggFieldName: metricKeySplit.length === 2 ? metricKeySplit[1] : undefined, + }; +} + +export function migrateJoinAggKey({ + attributes, +}: { + attributes: MapSavedObjectAttributes; +}): MapSavedObjectAttributes { + if (!attributes || !attributes.layerListJSON) { + return attributes; + } + + const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + layerList.forEach((layerDescriptor: LayerDescriptor) => { + if ( + layerDescriptor.type === LAYER_TYPE.VECTOR || + layerDescriptor.type === LAYER_TYPE.BLENDED_VECTOR + ) { + const vectorLayerDescriptor = layerDescriptor as VectorLayerDescriptor; + + if ( + !vectorLayerDescriptor.style || + !vectorLayerDescriptor.joins || + vectorLayerDescriptor.joins.length === 0 + ) { + return; + } + + const legacyJoinFields = new Map(); + vectorLayerDescriptor.joins.forEach((joinDescriptor: JoinDescriptor) => { + _.get(joinDescriptor, 'right.metrics', []).forEach((aggDescriptor: AggDescriptor) => { + const legacyAggKey = getLegacyAggKey({ + aggType: aggDescriptor.type, + aggFieldName: aggDescriptor.field, + indexPatternTitle: _.get(joinDescriptor, 'right.indexPatternTitle', ''), + termFieldName: _.get(joinDescriptor, 'right.term', ''), + }); + // The legacy getAggKey implemenation has a naming collision bug where + // aggType, aggFieldName, indexPatternTitle, and termFieldName would result in the identical aggKey. + // The VectorStyle implemenation used the first matching join descriptor + // so, in the event of a name collision, the first join descriptor will be used here as well. + if (!legacyJoinFields.has(legacyAggKey)) { + legacyJoinFields.set(legacyAggKey, joinDescriptor); + } + }); + }); + + Object.keys(vectorLayerDescriptor.style.properties).forEach(key => { + const style: any = vectorLayerDescriptor.style!.properties[key as VECTOR_STYLES]; + if (_.get(style, 'options.field.origin') === FIELD_ORIGIN.JOIN) { + const joinDescriptor = legacyJoinFields.get(style.options.field.name); + if (joinDescriptor) { + const { aggType, aggFieldName } = parseLegacyAggKey(style.options.field.name); + // Update legacy join agg key to new join agg key + style.options.field.name = getJoinAggKey({ + aggType, + aggFieldName, + rightSourceId: joinDescriptor.right.id!, + }); + } + } + }); + } + }); + + return { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }; +} diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 0d5f0f60fa36b..d7dfa4e894186 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -38,6 +38,7 @@ export function maps(kibana) { return { showMapVisualizationTypes: serverConfig.get('xpack.maps.showMapVisualizationTypes'), showMapsInspectorAdapter: serverConfig.get('xpack.maps.showMapsInspectorAdapter'), + enableVectorTiles: serverConfig.get('xpack.maps.enableVectorTiles'), preserveDrawingBuffer: serverConfig.get('xpack.maps.preserveDrawingBuffer'), isEmsEnabled: mapConfig.includeElasticMapsService, emsFontLibraryUrl: mapConfig.emsFontLibraryUrl, @@ -83,6 +84,7 @@ export function maps(kibana) { showMapVisualizationTypes: Joi.boolean().default(false), showMapsInspectorAdapter: Joi.boolean().default(false), // flag used in functional testing preserveDrawingBuffer: Joi.boolean().default(false), // flag used in functional testing + enableVectorTiles: Joi.boolean().default(false), // flag used to enable/disable vector-tiles }).default(); }, diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/legacy/plugins/maps/migrations.js index 6a1f5bc937497..a8e69eef7a02f 100644 --- a/x-pack/legacy/plugins/maps/migrations.js +++ b/x-pack/legacy/plugins/maps/migrations.js @@ -11,6 +11,7 @@ import { moveApplyGlobalQueryToSources } from './common/migrations/move_apply_gl import { addFieldMetaOptions } from './common/migrations/add_field_meta_options'; import { migrateSymbolStyleDescriptor } from './common/migrations/migrate_symbol_style_descriptor'; import { migrateUseTopHitsToScalingType } from './common/migrations/scaling_type'; +import { migrateJoinAggKey } from './common/migrations/join_agg_key'; export const migrations = { map: { @@ -57,5 +58,13 @@ export const migrations = { attributes: attributesPhase2, }; }, + '7.8.0': doc => { + const attributes = migrateJoinAggKey(doc); + + return { + ...doc, + attributes, + }; + }, }, }; diff --git a/x-pack/legacy/plugins/maps/public/plugin.ts b/x-pack/legacy/plugins/maps/public/plugin.ts index e4f831cf5487f..de9ac2c0ffc4a 100644 --- a/x-pack/legacy/plugins/maps/public/plugin.ts +++ b/x-pack/legacy/plugins/maps/public/plugin.ts @@ -4,13 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../plugins/maps/public/layers/layer_wizard_registry'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../plugins/maps/public/layers/sources/source_registry'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../plugins/maps/public/layers/load_layer_wizards'; - import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; // @ts-ignore import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; diff --git a/x-pack/legacy/plugins/rollup/index.ts b/x-pack/legacy/plugins/rollup/index.ts index 2c8363cc397f4..621667f3618b2 100644 --- a/x-pack/legacy/plugins/rollup/index.ts +++ b/x-pack/legacy/plugins/rollup/index.ts @@ -40,7 +40,7 @@ export function rollup(kibana: any) { }, init(server: any) { const { core: coreSetup, plugins } = server.newPlatform.setup; - const { usageCollection, metrics, indexManagement } = plugins; + const { usageCollection, visTypeTimeseries, indexManagement } = plugins; const rollupSetup = (plugins.rollup as unknown) as RollupSetup; @@ -53,7 +53,7 @@ export function rollup(kibana: any) { rollupPluginInstance.setup(coreSetup, { usageCollection, - metrics, + visTypeTimeseries, indexManagement, __LEGACY: { plugins: { diff --git a/x-pack/legacy/plugins/rollup/kibana.json b/x-pack/legacy/plugins/rollup/kibana.json index 3df8bd7c187d5..78458c9218be3 100644 --- a/x-pack/legacy/plugins/rollup/kibana.json +++ b/x-pack/legacy/plugins/rollup/kibana.json @@ -4,7 +4,7 @@ "requiredPlugins": [ "home", "index_management", - "metrics", + "visTypeTimeseries", "indexPatternManagement" ], "optionalPlugins": [ diff --git a/x-pack/legacy/plugins/rollup/server/plugin.ts b/x-pack/legacy/plugins/rollup/server/plugin.ts index 5f29ad160e052..05c22b030fff9 100644 --- a/x-pack/legacy/plugins/rollup/server/plugin.ts +++ b/x-pack/legacy/plugins/rollup/server/plugin.ts @@ -38,12 +38,12 @@ export class RollupsServerPlugin implements Plugin { { __LEGACY: serverShim, usageCollection, - metrics, + visTypeTimeseries, indexManagement, }: { __LEGACY: ServerShim; usageCollection?: UsageCollectionSetup; - metrics?: VisTypeTimeseriesSetup; + visTypeTimeseries?: VisTypeTimeseriesSetup; indexManagement?: IndexManagementPluginSetup; } ) { @@ -83,8 +83,8 @@ export class RollupsServerPlugin implements Plugin { indexManagement.indexDataEnricher.add(rollupDataEnricher); } - if (metrics) { - const { addSearchStrategy } = metrics; + if (visTypeTimeseries) { + const { addSearchStrategy } = visTypeTimeseries; registerRollupSearchStrategy(routeDependencies, addSearchStrategy); } } diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 22f1b3beffa35..e0acc7ecfe036 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -111,3 +111,20 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ ]; export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions'; export const NOTIFICATION_THROTTLE_RULE = 'rule'; + +/** + * Histograms for fields named in this list should be displayed with an + * "All others" bucket, to count events that don't specify a value for + * the field being counted + */ +export const showAllOthersBucket: string[] = [ + 'destination.ip', + 'event.action', + 'event.category', + 'event.dataset', + 'event.module', + 'signal.rule.threat.tactic.name', + 'source.ip', + 'destination.ip', + 'user.name', +]; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx index 272c41833f368..635d48cca10fc 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx @@ -4,15 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow, ShallowWrapper } from 'enzyme'; +import { Chart, BarSeries, Axis, ScaleType } from '@elastic/charts'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { escapeDataProviderId } from '../drag_and_drop/helpers'; +import { TestProviders } from '../../mock'; import { BarChartBaseComponent, BarChartComponent } from './barchart'; import { ChartSeriesData } from './common'; -import { Chart, BarSeries, Axis, ScaleType } from '@elastic/charts'; jest.mock('../../lib/kibana'); +jest.mock('uuid', () => { + return { + v1: jest.fn(() => 'uuid.v1()'), + v4: jest.fn(() => 'uuid.v4()'), + }; +}); + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const customHeight = '100px'; const customWidth = '120px'; const chartDataSets = [ @@ -116,6 +130,19 @@ const mockConfig = { customHeight: 324, }; +// Suppress warnings about "react-beautiful-dnd" +/* eslint-disable no-console */ +const originalError = console.error; +const originalWarn = console.warn; +beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); +}); +afterAll(() => { + console.error = originalError; + console.warn = originalWarn; +}); + describe('BarChartBaseComponent', () => { let shallowWrapper: ShallowWrapper; const mockBarChartData: ChartSeriesData[] = [ @@ -280,6 +307,91 @@ describe.each(chartDataSets)('BarChart with valid data [%o]', data => { expect(shallowWrapper.find('BarChartBase')).toHaveLength(1); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0); }); + + it('it does NOT render a draggable legend because stackByField is not provided', () => { + expect(shallowWrapper.find('[data-test-subj="draggable-legend"]').exists()).toBe(false); + }); +}); + +describe.each(chartDataSets)('BarChart with stackByField', () => { + let wrapper: ReactWrapper; + + const data = [ + { + key: 'python.exe', + value: [ + { + x: 1586754900000, + y: 9675, + g: 'python.exe', + }, + ], + }, + { + key: 'kernel', + value: [ + { + x: 1586754900000, + y: 8708, + g: 'kernel', + }, + { + x: 1586757600000, + y: 9282, + g: 'kernel', + }, + ], + }, + { + key: 'sshd', + value: [ + { + x: 1586754900000, + y: 5907, + g: 'sshd', + }, + ], + }, + ]; + + const expectedColors = ['#1EA593', '#2B70F7', '#CE0060']; + + const stackByField = 'process.name'; + + beforeAll(() => { + wrapper = mount( + + + + + + ); + }); + + it('it renders a draggable legend', () => { + expect(wrapper.find('[data-test-subj="draggable-legend"]').exists()).toBe(true); + }); + + expectedColors.forEach((color, i) => { + test(`it renders the expected legend color ${color} for legend item ${i}`, () => { + expect(wrapper.find(`div [color="${color}"]`).exists()).toBe(true); + }); + }); + + data.forEach(datum => { + test(`it renders the expected draggable legend text for datum ${datum.key}`, () => { + const dataProviderId = `draggableId.content.draggable-legend-item-uuid_v4()-${escapeDataProviderId( + stackByField + )}-${escapeDataProviderId(datum.key)}`; + + expect( + wrapper + .find(`div [data-rbd-draggable-id="${dataProviderId}"]`) + .first() + .text() + ).toEqual(datum.key); + }); + }); }); describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', data => { 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 2ae0e05850a37..64d15cd6731cb 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -4,13 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; import { Chart, BarSeries, Axis, Position, ScaleType, Settings } from '@elastic/charts'; import { getOr, get, isNumber } from 'lodash/fp'; import deepmerge from 'deepmerge'; +import uuid from 'uuid'; +import styled from 'styled-components'; -import { useThrottledResizeObserver } from '../utils'; +import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { useTimeZone } from '../../lib/kibana'; +import { defaultLegendColors } from '../matrix_histogram/utils'; +import { useThrottledResizeObserver } from '../utils'; + import { ChartPlaceHolder } from './chart_place_holder'; import { chartDefaultSettings, @@ -22,6 +28,12 @@ import { WrappedByAutoSizer, useTheme, } from './common'; +import { DraggableLegend } from './draggable_legend'; +import { LegendItem } from './draggable_legend_item'; + +const LegendFlexItem = styled(EuiFlexItem)` + overview: hidden; +`; const checkIfAllTheDataInTheSeriesAreValid = (series: ChartSeriesData): series is ChartSeriesData => series != null && @@ -38,12 +50,14 @@ const checkIfAnyValidSeriesExist = ( // Bar chart rotation: https://ela.st/chart-rotations export const BarChartBaseComponent = ({ data, + forceHiddenLegend = false, ...chartConfigs }: { data: ChartSeriesData[]; width: string | null | undefined; height: string | null | undefined; configs?: ChartSeriesConfigs | undefined; + forceHiddenLegend?: boolean; }) => { const theme = useTheme(); const timeZone = useTimeZone(); @@ -59,10 +73,10 @@ export const BarChartBaseComponent = ({ return chartConfigs.width && chartConfigs.height ? ( - + {data.map(series => { const barSeriesKey = series.key; - return checkIfAllTheDataInTheSeriesAreValid ? ( + return checkIfAllTheDataInTheSeriesAreValid(series) ? ( = ({ barChart, configs }) => { +const NO_LEGEND_DATA: LegendItem[] = []; + +export const BarChartComponent: React.FC = ({ + barChart, + configs, + stackByField, +}) => { const { ref: measureRef, width, height } = useThrottledResizeObserver(); + const legendItems: LegendItem[] = useMemo( + () => + barChart != null && stackByField != null + ? barChart.map((d, i) => ({ + color: d.color ?? i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, + dataProviderId: escapeDataProviderId( + `draggable-legend-item-${uuid.v4()}-${stackByField}-${d.key}` + ), + field: stackByField, + value: d.key, + })) + : NO_LEGEND_DATA, + [barChart, stackByField] + ); + const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); const chartHeight = getChartHeight(customHeight, height); const chartWidth = getChartWidth(customWidth, width); return checkIfAnyValidSeriesExist(barChart) ? ( - - - + + + + + + + + + + ) : ( ); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.test.tsx new file mode 100644 index 0000000000000..0da0c2bdc35f2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.test.tsx @@ -0,0 +1,149 @@ +/* + * 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { TestProviders } from '../../mock'; + +import { MIN_LEGEND_HEIGHT, DraggableLegend } from './draggable_legend'; +import { LegendItem } from './draggable_legend_item'; + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + +const allOthersDataProviderId = + 'draggable-legend-item-527adabe-8e1c-4a1f-965c-2f3d65dda9e1-event_dataset-All others'; + +const legendItems: LegendItem[] = [ + { + color: '#1EA593', + dataProviderId: 'draggable-legend-item-3207fda7-d008-402a-86a0-8ad632081bad-event_dataset-flow', + field: 'event.dataset', + value: 'flow', + }, + { + color: '#2B70F7', + dataProviderId: + 'draggable-legend-item-83f6c824-811d-4ec8-b373-eba2b0de6398-event_dataset-suricata_eve', + field: 'event.dataset', + value: 'suricata.eve', + }, + { + color: '#CE0060', + dataProviderId: + 'draggable-legend-item-ec57bb8f-82cd-4e07-bd38-1d11b3f0ee5f-event_dataset-traefik_access', + field: 'event.dataset', + value: 'traefik.access', + }, + { + color: '#38007E', + dataProviderId: + 'draggable-legend-item-25d5fcd6-87ba-46b5-893e-c655d7d504e3-event_dataset-esensor', + field: 'event.dataset', + value: 'esensor', + }, + { + color: '#F37020', + dataProviderId: allOthersDataProviderId, + field: 'event.dataset', + value: 'All others', + }, +]; + +describe('DraggableLegend', () => { + const height = 400; + + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + describe('rendering', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + + + + ); + }); + + it(`renders a container with the specified non-zero 'height'`, () => { + expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule( + 'height', + `${height}px` + ); + }); + + it('scrolls when necessary', () => { + expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule( + 'overflow', + 'auto' + ); + }); + + it('renders the legend items', () => { + legendItems.forEach(item => + expect( + wrapper + .find( + item.dataProviderId !== allOthersDataProviderId + ? `[data-test-subj="legend-item-${item.dataProviderId}"]` + : '[data-test-subj="all-others-legend-item"]' + ) + .first() + .text() + ).toEqual(item.value) + ); + }); + + it('renders a spacer for every legend item', () => { + expect(wrapper.find('[data-test-subj="draggable-legend-spacer"]').hostNodes().length).toEqual( + legendItems.length + ); + }); + }); + + it('does NOT render the legend when an empty collection of legendItems is provided', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="draggable-legend"]').exists()).toBe(false); + }); + + it(`renders a legend with the minimum height when 'height' is zero`, () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule( + 'height', + `${MIN_LEGEND_HEIGHT}px` + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.tsx b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.tsx new file mode 100644 index 0000000000000..ef3fbb8780d15 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.tsx @@ -0,0 +1,68 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; +import { rgba } from 'polished'; +import React from 'react'; +import styled from 'styled-components'; + +import { DraggableLegendItem, LegendItem } from './draggable_legend_item'; + +export const MIN_LEGEND_HEIGHT = 175; + +const DraggableLegendContainer = styled.div<{ height: number }>` + height: ${({ height }) => `${height}px`}; + overflow: auto; + scrollbar-width: thin; + width: 165px; + + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } +`; + +const DraggableLegendComponent: React.FC<{ + height: number; + legendItems: LegendItem[]; +}> = ({ height, legendItems }) => { + if (legendItems.length === 0) { + return null; + } + + return ( + + + + {legendItems.map(item => ( + + + + + ))} + + + + ); +}; + +DraggableLegendComponent.displayName = 'DraggableLegendComponent'; + +export const DraggableLegend = React.memo(DraggableLegendComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.test.tsx new file mode 100644 index 0000000000000..581952a8415f6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.test.tsx @@ -0,0 +1,143 @@ +/* + * 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { TestProviders } from '../../mock'; + +import { DraggableLegendItem, LegendItem } from './draggable_legend_item'; + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + +describe('DraggableLegendItem', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + describe('rendering a regular (non "All others") legend item', () => { + const legendItem: LegendItem = { + color: '#1EA593', + dataProviderId: + 'draggable-legend-item-3207fda7-d008-402a-86a0-8ad632081bad-event_dataset-flow', + field: 'event.dataset', + value: 'flow', + }; + + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + + + + ); + }); + + it('renders a colored circle with the expected legend item color', () => { + expect( + wrapper + .find('[data-test-subj="legend-color"]') + .first() + .props().color + ).toEqual(legendItem.color); + }); + + it('renders draggable legend item text', () => { + expect( + wrapper + .find(`[data-test-subj="legend-item-${legendItem.dataProviderId}"]`) + .first() + .text() + ).toEqual(legendItem.value); + }); + + it('does NOT render a non-draggable "All others" legend item', () => { + expect(wrapper.find(`[data-test-subj="all-others-legend-item"]`).exists()).toBe(false); + }); + }); + + describe('rendering an "All others" legend item', () => { + const allOthersLegendItem: LegendItem = { + color: '#F37020', + dataProviderId: + 'draggable-legend-item-527adabe-8e1c-4a1f-965c-2f3d65dda9e1-event_dataset-All others', + field: 'event.dataset', + value: 'All others', + }; + + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + + + + ); + }); + + it('renders a colored circle with the expected legend item color', () => { + expect( + wrapper + .find('[data-test-subj="legend-color"]') + .first() + .props().color + ).toEqual(allOthersLegendItem.color); + }); + + it('does NOT render a draggable legend item', () => { + expect( + wrapper + .find(`[data-test-subj="legend-item-${allOthersLegendItem.dataProviderId}"]`) + .exists() + ).toBe(false); + }); + + it('renders NON-draggable `All others` legend item text', () => { + expect( + wrapper + .find(`[data-test-subj="all-others-legend-item"]`) + .first() + .text() + ).toEqual(allOthersLegendItem.value); + }); + }); + + it('does NOT render a colored circle when the legend item has no color', () => { + const noColorLegendItem: LegendItem = { + // no `color` attribute for this `LegendItem`! + dataProviderId: + 'draggable-legend-item-3207fda7-d008-402a-86a0-8ad632081bad-event_dataset-flow', + field: 'event.dataset', + value: 'flow', + }; + + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="legend-color"]').exists()).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.tsx b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.tsx new file mode 100644 index 0000000000000..cdda1733932d5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.tsx @@ -0,0 +1,62 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { DefaultDraggable } from '../draggables'; + +import * as i18n from './translation'; + +// The "All others" legend item is not draggable +const AllOthers = styled.span` + padding-left: 7px; +`; + +export interface LegendItem { + color?: string; + dataProviderId: string; + field: string; + value: string; +} + +const DraggableLegendItemComponent: React.FC<{ + legendItem: LegendItem; +}> = ({ legendItem }) => { + const { color, dataProviderId, field, value } = legendItem; + + return ( + + + {color != null && ( + + + + )} + + + {value !== i18n.ALL_OTHERS ? ( + + ) : ( + <> + {value} + + )} + + + + ); +}; + +DraggableLegendItemComponent.displayName = 'DraggableLegendItemComponent'; + +export const DraggableLegendItem = React.memo(DraggableLegendItemComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/translation.ts b/x-pack/legacy/plugins/siem/public/components/charts/translation.ts index 341cb7782f87c..891f59fc97bd1 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/translation.ts +++ b/x-pack/legacy/plugins/siem/public/components/charts/translation.ts @@ -13,3 +13,7 @@ export const ALL_VALUES_ZEROS_TITLE = i18n.translate('xpack.siem.chart.dataAllVa export const DATA_NOT_AVAILABLE_TITLE = i18n.translate('xpack.siem.chart.dataNotAvailableTitle', { defaultMessage: 'Chart Data Not Available', }); + +export const ALL_OTHERS = i18n.translate('xpack.siem.chart.allOthersGroupingLabel', { + defaultMessage: 'All others', +}); 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 11db33fff6d72..248ae671550ef 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 @@ -27,6 +27,9 @@ import { draggableIsField, } from './helpers'; +// @ts-ignore +window['__react-beautiful-dnd-disable-dev-warnings'] = true; + interface Props { browserFields: BrowserFields; children: React.ReactNode; 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 11891afabbf3d..cd9e1dc95ff01 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 @@ -7,12 +7,13 @@ import { shallow } from 'enzyme'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; +import { DraggableStateSnapshot, DraggingStyle } from 'react-beautiful-dnd'; import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; -import { DraggableWrapper, ConditionalPortal } from './draggable_wrapper'; +import { ConditionalPortal, DraggableWrapper, getStyle } from './draggable_wrapper'; import { useMountAppended } from '../../utils/use_mount_appended'; describe('DraggableWrapper', () => { @@ -48,6 +49,36 @@ describe('DraggableWrapper', () => { expect(wrapper.text()).toEqual(message); }); + + test('it does NOT render hover actions when the mouse is NOT over the draggable wrapper', () => { + const wrapper = mount( + + + + message} /> + + + + ); + + expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(false); + }); + + test('it renders hover actions when the mouse is over the draggable wrapper', () => { + const wrapper = mount( + + + + message} /> + + + + ); + + wrapper.simulate('mouseenter'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + }); }); describe('text truncation styling', () => { @@ -100,4 +131,36 @@ describe('ConditionalPortal', () => { expect(props.registerProvider.mock.calls.length).toEqual(1); }); + + describe('getStyle', () => { + const style: DraggingStyle = { + boxSizing: 'border-box', + height: 10, + left: 1, + pointerEvents: 'none', + position: 'fixed', + transition: 'none', + top: 123, + width: 50, + zIndex: 9999, + }; + + it('returns a style with no transitionDuration when the snapshot is not drop animating', () => { + const snapshot: DraggableStateSnapshot = { + isDragging: true, + isDropAnimating: false, // <-- NOT drop animating + }; + + expect(getStyle(style, snapshot)).not.toHaveProperty('transitionDuration'); + }); + + it('returns a style with a transitionDuration when the snapshot is drop animating', () => { + const snapshot: DraggableStateSnapshot = { + isDragging: true, + isDropAnimating: true, // <-- it is drop animating + }; + + expect(getStyle(style, snapshot)).toHaveProperty('transitionDuration', '0.00000001s'); + }); + }); }); 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 3a6a4de7984db..c7da5b5c58951 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 @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Draggable, DraggableProvided, DraggableStateSnapshot, + DraggingStyle, Droppable, + NotDraggingStyle, } from 'react-beautiful-dnd'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; @@ -18,6 +20,9 @@ import deepEqual from 'fast-deep-equal'; import { dragAndDropActions } from '../../store/drag_and_drop'; import { DataProvider } from '../timeline/data_providers/data_provider'; import { TruncatableText } from '../truncatable_text'; +import { WithHoverActions } from '../with_hover_actions'; + +import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; import { getDraggableId, getDroppableId } from './helpers'; import { ProviderContainer } from './provider_container'; @@ -67,23 +72,42 @@ type RenderFunctionProp = ( state: DraggableStateSnapshot ) => React.ReactNode; -interface OwnProps { +interface Props { dataProvider: DataProvider; inline?: boolean; render: RenderFunctionProp; truncate?: boolean; + onFilterAdded?: () => void; } -type Props = OwnProps; - /** * Wraps a draggable component to handle registration / unregistration of the * data provider associated with the item being dropped */ +export const getStyle = ( + style: DraggingStyle | NotDraggingStyle | undefined, + snapshot: DraggableStateSnapshot +) => { + if (!snapshot.isDropAnimating) { + return style; + } + + return { + ...style, + transitionDuration: '0.00000001s', // cannot be 0, but can be a very short duration + }; +}; + export const DraggableWrapper = React.memo( - ({ dataProvider, render, truncate }) => { + ({ dataProvider, onFilterAdded, render, truncate }) => { + const [showTopN, setShowTopN] = useState(false); + const toggleTopN = useCallback(() => { + setShowTopN(!showTopN); + }, [setShowTopN, showTopN]); + const [providerRegistered, setProviderRegistered] = useState(false); + const dispatch = useDispatch(); const registerProvider = useCallback(() => { @@ -105,65 +129,90 @@ export const DraggableWrapper = React.memo( [] ); - return ( - - - ( - -
- ( + + ), + [dataProvider, onFilterAdded, showTopN, toggleTopN] + ); + + const renderContent = useCallback( + () => ( + + + ( + +
- {render(dataProvider, provided, snapshot)} - -
-
- )} - > - {droppableProvided => ( -
- - {(provided, snapshot) => ( - - {truncate && !snapshot.isDragging ? ( - - {render(dataProvider, provided, snapshot)} - - ) : ( - - {render(dataProvider, provided, snapshot)} - - )} - - )} - - {droppableProvided.placeholder} -
- )} -
-
-
+ {render(dataProvider, provided, snapshot)} +
+
+
+ )} + > + {droppableProvided => ( +
+ + {(provided, snapshot) => ( + + {truncate && !snapshot.isDragging ? ( + + {render(dataProvider, provided, snapshot)} + + ) : ( + + {render(dataProvider, provided, snapshot)} + + )} + + )} + + {droppableProvided.placeholder} +
+ )} +
+
+
+ ), + [dataProvider, render, registerProvider, truncate] + ); + + return ( + ); }, (prevProps, nextProps) => diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx new file mode 100644 index 0000000000000..f8b5eb7209ff4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -0,0 +1,559 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; + +import { mocksSource } from '../../containers/source/mock'; +import { wait } from '../../lib/helpers'; +import { useKibana } from '../../lib/kibana'; +import { TestProviders } from '../../mock'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { TimelineContext } from '../timeline/timeline_context'; + +import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; + +jest.mock('../../lib/kibana'); + +jest.mock('uuid', () => { + return { + v1: jest.fn(() => 'uuid.v1()'), + v4: jest.fn(() => 'uuid.v4()'), + }; +}); + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; +const field = 'process.name'; +const value = 'nice'; + +describe('DraggableWrapperHoverContent', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + /** + * The tests for "Filter for value" and "Filter out value" are similar enough + * to combine them into "table tests" using this array + */ + const forOrOut = ['for', 'out']; + + forOrOut.forEach(hoverAction => { + describe(`Filter ${hoverAction} value`, () => { + test(`it renders the 'Filter ${hoverAction} value' button when showTopN is false`, () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .exists() + ).toBe(true); + }); + + test(`it does NOT render the 'Filter ${hoverAction} value' button when showTopN is true`, () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .exists() + ).toBe(false); + }); + + describe('when run in the context of a timeline', () => { + let filterManager: FilterManager; + let wrapper: ReactWrapper; + let onFilterAdded: () => void; + + beforeEach(() => { + filterManager = new FilterManager(mockUiSettingsForFilterManager); + filterManager.addFilters = jest.fn(); + onFilterAdded = jest.fn(); + + wrapper = mount( + + + + + + ); + }); + + test('when clicked, it adds a filter to the timeline when running in the context of a timeline', () => { + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(filterManager.addFilters).toBeCalledWith({ + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: hoverAction === 'out' ? true : false, + params: { query: 'nice' }, + type: 'phrase', + value: 'nice', + }, + query: { match: { 'process.name': { query: 'nice', type: 'phrase' } } }, + }); + }); + + test('when clicked, invokes onFilterAdded when running in the context of a timeline', () => { + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(onFilterAdded).toBeCalled(); + }); + }); + + describe('when NOT run in the context of a timeline', () => { + let wrapper: ReactWrapper; + let onFilterAdded: () => void; + const kibana = useKibana(); + + beforeEach(() => { + kibana.services.data.query.filterManager.addFilters = jest.fn(); + onFilterAdded = jest.fn(); + + wrapper = mount( + + + + ); + }); + + test('when clicked, it adds a filter to the global filters when NOT running in the context of a timeline', () => { + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(kibana.services.data.query.filterManager.addFilters).toBeCalledWith({ + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: hoverAction === 'out' ? true : false, + params: { query: 'nice' }, + type: 'phrase', + value: 'nice', + }, + query: { match: { 'process.name': { query: 'nice', type: 'phrase' } } }, + }); + }); + + test('when clicked, invokes onFilterAdded when NOT running in the context of a timeline', () => { + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(onFilterAdded).toBeCalled(); + }); + }); + + describe('an empty string value when run in the context of a timeline', () => { + let filterManager: FilterManager; + let wrapper: ReactWrapper; + let onFilterAdded: () => void; + + beforeEach(() => { + filterManager = new FilterManager(mockUiSettingsForFilterManager); + filterManager.addFilters = jest.fn(); + onFilterAdded = jest.fn(); + + wrapper = mount( + + + + + + ); + }); + + const expectedFilterTypeDescription = + hoverAction === 'for' ? 'a "NOT exists"' : 'an "exists"'; + test(`when clicked, it adds ${expectedFilterTypeDescription} filter to the timeline when run in the context of a timeline`, () => { + const expected = + hoverAction === 'for' + ? { + exists: { field: 'process.name' }, + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: true, + type: 'exists', + value: 'exists', + }, + } + : { + exists: { field: 'process.name' }, + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: false, + type: 'exists', + value: 'exists', + }, + }; + + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(filterManager.addFilters).toBeCalledWith(expected); + }); + }); + + describe('an empty string value when NOT run in the context of a timeline', () => { + let wrapper: ReactWrapper; + let onFilterAdded: () => void; + const kibana = useKibana(); + + beforeEach(() => { + kibana.services.data.query.filterManager.addFilters = jest.fn(); + onFilterAdded = jest.fn(); + + wrapper = mount( + + + + ); + }); + + const expectedFilterTypeDescription = + hoverAction === 'for' ? 'a "NOT exists"' : 'an "exists"'; + test(`when clicked, it adds ${expectedFilterTypeDescription} filter to the global filters when NOT running in the context of a timeline`, () => { + const expected = + hoverAction === 'for' + ? { + exists: { field: 'process.name' }, + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: true, + type: 'exists', + value: 'exists', + }, + } + : { + exists: { field: 'process.name' }, + meta: { + alias: null, + disabled: false, + key: 'process.name', + negate: false, + type: 'exists', + value: 'exists', + }, + }; + + wrapper + .find(`[data-test-subj="filter-${hoverAction}-value"]`) + .first() + .simulate('click'); + wrapper.update(); + + expect(kibana.services.data.query.filterManager.addFilters).toBeCalledWith(expected); + }); + }); + }); + }); + + describe('Top N', () => { + test(`it renders the 'Show top field' button when showTopN is false and an aggregatable string field is provided`, async () => { + const aggregatableStringField = 'cloud.account.id'; + const wrapper = mount( + + + + + + ); + + await wait(); // https://github.com/apollographql/react-apollo/issues/1711 + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="show-top-field"]') + .first() + .exists() + ).toBe(true); + }); + + test(`it renders the 'Show top field' button when showTopN is false and a whitelisted signal field is provided`, async () => { + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="show-top-field"]') + .first() + .exists() + ).toBe(true); + }); + + test(`it does NOT render the 'Show top field' button when showTopN is false and a field not known to BrowserFields is provided`, async () => { + const notKnownToBrowserFields = 'unknown.field'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="show-top-field"]') + .first() + .exists() + ).toBe(false); + }); + + test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, async () => { + const toggleTopN = jest.fn(); + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + wrapper + .find('[data-test-subj="show-top-field"]') + .first() + .simulate('click'); + wrapper.update(); + + expect(toggleTopN).toBeCalled(); + }); + + test(`it does NOT render the Top N histogram when when showTopN is false`, async () => { + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="eventsByDatasetOverviewPanel"]') + .first() + .exists() + ).toBe(false); + }); + + test(`it does NOT render the 'Show top field' button when showTopN is true`, async () => { + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="show-top-field"]') + .first() + .exists() + ).toBe(false); + }); + + test(`it renders the Top N histogram when when showTopN is true`, async () => { + const whitelistedField = 'signal.rule.name'; + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]') + .first() + .exists() + ).toBe(true); + }); + }); + + describe('Copy to Clipboard', () => { + test(`it renders the 'Copy to Clipboard' button when showTopN is false`, () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="copy-to-clipboard"]`) + .first() + .exists() + ).toBe(true); + }); + + test(`it does NOT render the 'Copy to Clipboard' button when showTopN is true`, () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="copy-to-clipboard"]`) + .first() + .exists() + ).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx new file mode 100644 index 0000000000000..40725bea498f1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -0,0 +1,145 @@ +/* + * 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; + +import { getAllFieldsByName, WithSource } from '../../containers/source'; +import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; +import { useKibana } from '../../lib/kibana'; +import { createFilter } from '../page/add_filter_to_global_search_bar'; +import { useTimelineContext } from '../timeline/timeline_context'; +import { StatefulTopN } from '../top_n'; + +import { allowTopN } from './helpers'; +import * as i18n from './translations'; + +interface Props { + field: string; + onFilterAdded?: () => void; + showTopN: boolean; + toggleTopN: () => void; + value?: string[] | string | null; +} + +const DraggableWrapperHoverContentComponent: React.FC = ({ + field, + onFilterAdded, + showTopN, + toggleTopN, + value, +}) => { + const kibana = useKibana(); + const { filterManager: timelineFilterManager } = useTimelineContext(); + const filterManager = useMemo(() => kibana.services.data.query.filterManager, [ + kibana.services.data.query.filterManager, + ]); + + const filterForValue = useCallback(() => { + const filter = + value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); + const activeFilterManager = timelineFilterManager ?? filterManager; + + if (activeFilterManager != null) { + activeFilterManager.addFilters(filter); + + if (onFilterAdded != null) { + onFilterAdded(); + } + } + }, [field, value, timelineFilterManager, filterManager, onFilterAdded]); + + const filterOutValue = useCallback(() => { + const filter = + value?.length === 0 ? createFilter(field, null, false) : createFilter(field, value, true); + const activeFilterManager = timelineFilterManager ?? filterManager; + + if (activeFilterManager != null) { + activeFilterManager.addFilters(filter); + + if (onFilterAdded != null) { + onFilterAdded(); + } + } + }, [field, value, timelineFilterManager, filterManager, onFilterAdded]); + + return ( + <> + {!showTopN && value != null && ( + + + + )} + + {!showTopN && value != null && ( + + + + )} + + + {({ browserFields }) => ( + <> + {allowTopN({ + browserField: getAllFieldsByName(browserFields)[field], + fieldName: field, + }) && ( + <> + {!showTopN && ( + + + + )} + + {showTopN && ( + + )} + + )} + + )} + + + {!showTopN && ( + + + + )} + + ); +}; + +DraggableWrapperHoverContentComponent.displayName = 'DraggableWrapperHoverContentComponent'; + +export const DraggableWrapperHoverContent = React.memo(DraggableWrapperHoverContentComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts index af4b9b280f3cd..753fa5b54eade 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { omit } from 'lodash/fp'; + import { + allowTopN, destinationIsTimelineButton, destinationIsTimelineColumns, destinationIsTimelineProviders, @@ -717,4 +720,96 @@ describe('helpers', () => { expect(escaped).toEqual('hello.how.are.you?'); }); }); + + describe('#allowTopN', () => { + const aggregatableAllowedType = { + 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', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + aggregatable: true, + format: '', + }; + + test('it returns true for an aggregatable field that is an allowed type', () => { + expect( + allowTopN({ + browserField: aggregatableAllowedType, + fieldName: aggregatableAllowedType.name, + }) + ).toBe(true); + }); + + test('it returns true for a whitelisted non-BrowserField', () => { + expect( + allowTopN({ + browserField: undefined, + fieldName: 'signal.rule.name', + }) + ).toBe(true); + }); + + test('it returns false for a NON-aggregatable field that is an allowed type', () => { + const nonAggregatableAllowedType = { + ...aggregatableAllowedType, + aggregatable: false, + }; + + expect( + allowTopN({ + browserField: nonAggregatableAllowedType, + fieldName: nonAggregatableAllowedType.name, + }) + ).toBe(false); + }); + + test('it returns false for a aggregatable field that is NOT an allowed type', () => { + const aggregatableNotAllowedType = { + ...aggregatableAllowedType, + type: 'not-an-allowed-type', + }; + + expect( + allowTopN({ + browserField: aggregatableNotAllowedType, + fieldName: aggregatableNotAllowedType.name, + }) + ).toBe(false); + }); + + test('it returns false if the BrowserField is missing the aggregatable property', () => { + const missingAggregatable = omit('aggregatable', aggregatableAllowedType); + + expect( + allowTopN({ + browserField: missingAggregatable, + fieldName: missingAggregatable.name, + }) + ).toBe(false); + }); + + test('it returns false if the BrowserField is missing the type property', () => { + const missingType = omit('type', aggregatableAllowedType); + + expect( + allowTopN({ + browserField: missingType, + fieldName: missingType.name, + }) + ).toBe(false); + }); + + test('it returns false for a non-whitelisted field when a BrowserField is not provided', () => { + expect( + allowTopN({ + browserField: undefined, + fieldName: 'non-whitelisted', + }) + ).toBe(false); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts index 82ddd2c9f29d7..cd3d7cc68d537 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts @@ -9,7 +9,7 @@ import { DropResult } from 'react-beautiful-dnd'; import { Dispatch } from 'redux'; import { ActionCreator } from 'typescript-fsa'; -import { BrowserFields, getAllFieldsByName } from '../../containers/source'; +import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source'; import { IdToDataProvider } from '../../store/drag_and_drop/model'; import { ColumnHeaderOptions } from '../../store/timeline/model'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; @@ -227,3 +227,98 @@ export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; /** This class is added to the document body while timeline field dragging */ export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging'; + +export const allowTopN = ({ + browserField, + fieldName, +}: { + browserField: Partial | undefined; + fieldName: string; +}): boolean => { + const isAggregatable = browserField?.aggregatable ?? false; + const fieldType = browserField?.type ?? ''; + const isAllowedType = [ + 'boolean', + 'geo-point', + 'geo-shape', + 'ip', + 'keyword', + 'number', + 'numeric', + 'string', + ].includes(fieldType); + + // TODO: remove this explicit whitelist when the ECS documentation includes signals + const isWhitelistedNonBrowserField = [ + 'signal.ancestors.depth', + 'signal.ancestors.id', + 'signal.ancestors.rule', + 'signal.ancestors.type', + 'signal.original_event.action', + 'signal.original_event.category', + 'signal.original_event.code', + 'signal.original_event.created', + 'signal.original_event.dataset', + 'signal.original_event.duration', + 'signal.original_event.end', + 'signal.original_event.hash', + 'signal.original_event.id', + 'signal.original_event.kind', + 'signal.original_event.module', + 'signal.original_event.original', + 'signal.original_event.outcome', + 'signal.original_event.provider', + 'signal.original_event.risk_score', + 'signal.original_event.risk_score_norm', + 'signal.original_event.sequence', + 'signal.original_event.severity', + 'signal.original_event.start', + 'signal.original_event.timezone', + 'signal.original_event.type', + 'signal.original_time', + 'signal.parent.depth', + 'signal.parent.id', + 'signal.parent.index', + 'signal.parent.rule', + 'signal.parent.type', + 'signal.rule.created_by', + 'signal.rule.description', + 'signal.rule.enabled', + 'signal.rule.false_positives', + 'signal.rule.filters', + 'signal.rule.from', + 'signal.rule.id', + 'signal.rule.immutable', + 'signal.rule.index', + 'signal.rule.interval', + 'signal.rule.language', + 'signal.rule.max_signals', + 'signal.rule.name', + 'signal.rule.note', + 'signal.rule.output_index', + 'signal.rule.query', + 'signal.rule.references', + 'signal.rule.risk_score', + 'signal.rule.rule_id', + 'signal.rule.saved_id', + 'signal.rule.severity', + 'signal.rule.size', + 'signal.rule.tags', + 'signal.rule.threat', + 'signal.rule.threat.tactic.id', + 'signal.rule.threat.tactic.name', + 'signal.rule.threat.tactic.reference', + 'signal.rule.threat.technique.id', + 'signal.rule.threat.technique.name', + 'signal.rule.threat.technique.reference', + 'signal.rule.timeline_id', + 'signal.rule.timeline_title', + 'signal.rule.to', + 'signal.rule.type', + 'signal.rule.updated_by', + 'signal.rule.version', + 'signal.status', + ].includes(fieldName); + + return isWhitelistedNonBrowserField || (isAggregatable && isAllowedType); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/translations.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/translations.ts new file mode 100644 index 0000000000000..61d036635a250 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/translations.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 { i18n } from '@kbn/i18n'; + +export const COPY_TO_CLIPBOARD = i18n.translate('xpack.siem.dragAndDrop.copyToClipboardTooltip', { + defaultMessage: 'Copy to Clipboard', +}); + +export const FIELD = i18n.translate('xpack.siem.dragAndDrop.fieldLabel', { + defaultMessage: 'Field', +}); + +export const FILTER_FOR_VALUE = i18n.translate('xpack.siem.dragAndDrop.filterForValueHoverAction', { + defaultMessage: 'Filter for value', +}); + +export const FILTER_OUT_VALUE = i18n.translate('xpack.siem.dragAndDrop.filterOutValueHoverAction', { + defaultMessage: 'Filter out value', +}); + +export const CLOSE = i18n.translate('xpack.siem.dragAndDrop.closeButtonLabel', { + defaultMessage: 'Close', +}); + +export const SHOW_TOP = (fieldName: string) => + i18n.translate('xpack.siem.overview.showTopTooltip', { + values: { fieldName }, + defaultMessage: `Show top {fieldName}`, + }); 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 63ba13306ecd8..93608a181adff 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 @@ -10,6 +10,7 @@ exports[`draggables rendering it renders the default Badge 1`] = ` A child of this 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 1fe6c936d2823..a0b825ea24441 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx @@ -167,7 +167,7 @@ export const DraggableBadge = React.memo( tooltipContent={tooltipContent} queryValue={queryValue} > - + {children ? children : value !== '' ? value : getEmptyStringTag()} diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx index cd94a9fdcb5ac..131a3a63bae30 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx @@ -21,7 +21,6 @@ import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; import { ToStringArray } from '../../graphql/types'; -import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { ColumnHeaderOptions } from '../../store/timeline/model'; import { DragEffects } from '../drag_and_drop/draggable_wrapper'; import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; @@ -35,7 +34,6 @@ import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; import { MESSAGE_FIELD_NAME } from '../timeline/body/renderers/constants'; import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; import { OnUpdateColumns } from '../timeline/events'; -import { WithHoverActions } from '../with_hover_actions'; import { getIconFromType, getExampleText, getColumnsWithTimestamp } from './helpers'; import * as i18n from './translations'; import { EventFieldsData } from './types'; @@ -172,29 +170,18 @@ export const getColumns = ({ component="span" key={`event-details-value-flex-item-${contextId}-${eventId}-${data.field}-${i}-${value}`} > - - - - - - } - render={() => - data.field === MESSAGE_FIELD_NAME ? ( - - ) : ( - - ) - } - /> + {data.field === MESSAGE_FIELD_NAME ? ( + + ) : ( + + )} ))} diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index ea2cb661763fa..d210c749dae9c 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -95,6 +95,7 @@ const EventsViewerComponent: React.FC = ({ }) => { const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); + const { filterManager } = useKibana().services.data.query; const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), dataProviders, @@ -168,7 +169,11 @@ const EventsViewerComponent: React.FC = ({ {utilityBar?.(refetch, totalCountMinusDeleted)} - + { }); }); - test('it renders a hover actions panel for the category name', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('[data-test-subj="category-link"]') - .first() - .find('[data-test-subj="hover-actions-panel-container"]') - .first() - .exists() - ).toBe(true); - }); - test('it renders the selected category with bold text', () => { const selectedCategoryId = 'auditd'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx index 0c7dd7e908ce3..7133e9b848c5c 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx @@ -6,15 +6,7 @@ /* eslint-disable react/display-name */ -import { - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiPanel, - EuiText, - EuiToolTip, -} from '@elastic/eui'; +import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; import React, { useContext } from 'react'; import styled from 'styled-components'; @@ -35,22 +27,6 @@ const CategoryName = styled.span<{ bold: boolean }>` CategoryName.displayName = 'CategoryName'; -const HoverActionsContainer = styled(EuiPanel)` - cursor: default; - left: 5px; - padding: 8px; - position: absolute; - top: -8px; -`; - -HoverActionsContainer.displayName = 'HoverActionsContainer'; - -const HoverActionsFlexGroup = styled(EuiFlexGroup)` - cursor: pointer; -`; - -HoverActionsFlexGroup.displayName = 'HoverActionsFlexGroup'; - const LinkContainer = styled.div` width: 100%; .euiLink { @@ -71,7 +47,7 @@ interface ToolTipProps { } const ToolTip = React.memo(({ categoryId, browserFields, onUpdateColumns }) => { - const isLoading = useContext(TimelineContext); + const { isLoading } = useContext(TimelineContext); return ( {!isLoading ? ( @@ -127,25 +103,11 @@ export const getCategoryColumns = ({ - - - - - - + } render={() => ( { expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); }); - test('it renders a view category action menu item a user hovers over the name', () => { - const wrapper = mount( - - - - ); - - wrapper.simulate('mouseenter'); - wrapper.update(); - expect(wrapper.find('[data-test-subj="view-category"]').exists()).toBe(true); - }); - - test('it invokes onUpdateColumns when the view category action menu item is clicked', () => { - const onUpdateColumns = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.simulate('mouseenter'); - wrapper.update(); - wrapper - .find('[data-test-subj="view-category"]') - .first() - .simulate('click'); - - expect(onUpdateColumns).toBeCalledWith([ - { - aggregatable: true, - category: 'base', - columnHeaderType: 'not-filtered', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - id: '@timestamp', - type: 'date', - width: 190, - }, - ]); - }); - test('it highlights the text specified by the `highlight` prop', () => { const highlight = 'stamp'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx index fe434a6ad63ce..fc9633b6f8748 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx @@ -4,26 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonIcon, - EuiHighlight, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiText, - EuiToolTip, -} from '@elastic/eui'; -import React, { useContext } from 'react'; +import { EuiButtonIcon, EuiHighlight, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; +import React, { useCallback, useContext, useState, useMemo } from 'react'; import styled from 'styled-components'; -import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { ColumnHeaderOptions } from '../../store/timeline/model'; import { OnUpdateColumns } from '../timeline/events'; import { TimelineContext } from '../timeline/timeline_context'; import { WithHoverActions } from '../with_hover_actions'; import { LoadingSpinner } from './helpers'; import * as i18n from './translations'; +import { DraggableWrapperHoverContent } from '../drag_and_drop/draggable_wrapper_hover_content'; /** * The name of a (draggable) field @@ -82,22 +73,6 @@ export const FieldNameContainer = styled.span` FieldNameContainer.displayName = 'FieldNameContainer'; -const HoverActionsContainer = styled(EuiPanel)` - cursor: default; - left: 5px; - padding: 4px; - position: absolute; - top: -6px; -`; - -HoverActionsContainer.displayName = 'HoverActionsContainer'; - -const HoverActionsFlexGroup = styled(EuiFlexGroup)` - cursor: pointer; -`; - -HoverActionsFlexGroup.displayName = 'HoverActionsFlexGroup'; - const ViewCategoryIcon = styled(EuiIcon)` margin-left: 5px; `; @@ -112,7 +87,7 @@ interface ToolTipProps { const ViewCategory = React.memo( ({ categoryId, onUpdateColumns, categoryColumns }) => { - const isLoading = useContext(TimelineContext); + const { isLoading } = useContext(TimelineContext); return ( {!isLoading ? ( @@ -142,48 +117,33 @@ export const FieldName = React.memo<{ fieldId: string; highlight?: string; onUpdateColumns: OnUpdateColumns; -}>(({ categoryId, categoryColumns, fieldId, highlight = '', onUpdateColumns }) => ( - - - - - - - - - {categoryColumns.length > 0 && ( - - - - )} - - - } - render={() => ( - - +}>(({ fieldId, highlight = '' }) => { + const [showTopN, setShowTopN] = useState(false); + const toggleTopN = useCallback(() => { + setShowTopN(!showTopN); + }, [setShowTopN, showTopN]); + + const hoverContent = useMemo( + () => ( + + ), + [fieldId, showTopN, toggleTopN] + ); + + const render = useCallback( + () => ( + + {fieldId} - - - )} - /> -)); + + + ), + [fieldId, highlight] + ); + + return ; +}); FieldName.displayName = 'FieldName'; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx index a1f3cfd857148..59039ddd6a23b 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx @@ -51,7 +51,9 @@ const TitleComponent: React.FC = ({ draggableArguments, title, badgeOptio tooltipPosition="bottom" /> ) : ( - {badgeOptions.text} + + {badgeOptions.text} + )} )} 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 d4c3763f51460..53b41e2240de2 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 @@ -11,13 +11,18 @@ exports[`HeaderSection it renders 1`] = ` responsive={false} > - +

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 bc4692b6fe0c5..e61b39691203c 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 @@ -48,7 +48,7 @@ describe('HeaderSection', () => { ).toBe(true); }); - test('it DOES NOT render the subtitle when not provided', () => { + test('renders the subtitle when not provided (to prevent layout thrash)', () => { const wrapper = mount( @@ -60,7 +60,7 @@ describe('HeaderSection', () => { .find('[data-test-subj="header-section-subtitle"]') .first() .exists() - ).toBe(false); + ).toBe(true); }); test('it renders supplements when children provided', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx index 3153e785a8a32..43245121dd393 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle, EuiTitleSize } from '@elastic/eui'; import React from 'react'; import styled, { css } from 'styled-components'; @@ -36,6 +36,7 @@ export interface HeaderSectionProps extends HeaderProps { split?: boolean; subtitle?: string | React.ReactNode; title: string | React.ReactNode; + titleSize?: EuiTitleSize; tooltip?: string; } @@ -46,6 +47,7 @@ const HeaderSectionComponent: React.FC = ({ split, subtitle, title, + titleSize = 'm', tooltip, }) => (
@@ -53,7 +55,7 @@ const HeaderSectionComponent: React.FC = ({ - +

{title} {tooltip && ( @@ -65,7 +67,7 @@ const HeaderSectionComponent: React.FC = ({

- {subtitle && } +
{id && ( diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx index d2d1d6569854d..214c0294f2cf4 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx @@ -24,6 +24,8 @@ import { ExternalLink, } from '.'; +jest.mock('../../pages/overview/events_by_dataset'); + jest.mock('../../lib/kibana', () => { return { useUiSetting$: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap index 0e518e48e2e88..5aa846d15b684 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"
"`; +exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"
"`; -exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"
"`; +exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"
"`; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx index db5b1f7f03ee3..3b8a43a0f395a 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx @@ -87,6 +87,17 @@ describe('Matrix Histogram Component', () => { }); }); + describe('spacer', () => { + test('it renders a spacer by default', () => { + expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(true); + }); + + test('it does NOT render a spacer when showSpacer is false', () => { + wrapper = mount(); + expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(false); + }); + }); + describe('not initial load', () => { beforeAll(() => { (useQuery as jest.Mock).mockReturnValue({ diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx index 12a474009dc5b..3d4eebd68319c 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx @@ -37,6 +37,7 @@ import { import { SetQuery } from '../../pages/hosts/navigation/types'; import { QueryTemplateProps } from '../../containers/query_template'; import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { InputsModelId } from '../../store/inputs/constants'; import { HistogramType } from '../../graphql/types'; export interface OwnProps extends QueryTemplateProps { @@ -46,9 +47,12 @@ export interface OwnProps extends QueryTemplateProps { hideHistogramIfEmpty?: boolean; histogramType: HistogramType; id: string; + indexToAdd?: string[] | null; legendPosition?: Position; mapping?: MatrixHistogramMappingTypes; + showSpacer?: boolean; setQuery: SetQuery; + setAbsoluteRangeDatePickerTarget?: InputsModelId; showLegend?: boolean; stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; @@ -62,6 +66,7 @@ const HeaderChildrenFlexItem = styled(EuiFlexItem)` margin-left: 24px; `; +// @ts-ignore - the EUI type definitions for Panel do no play nice with styled-components const HistogramPanel = styled(Panel)<{ height?: number }>` display: flex; flex-direction: column; @@ -79,16 +84,20 @@ export const MatrixHistogramComponent: React.FC { @@ -100,7 +109,11 @@ export const MatrixHistogramComponent: React.FC { - dispatchSetAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + dispatchSetAbsoluteRangeDatePicker({ + id: setAbsoluteRangeDatePickerTarget, + from: min, + to: max, + }); }, yTickFormatter, showLegend, @@ -122,7 +135,7 @@ export const MatrixHistogramComponent: React.FC) => { setSelectedStackByOption( - stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption + stackByOptions.find(co => co.value === event.target.value) ?? defaultStackByOption ); }, [] @@ -134,6 +147,7 @@ export const MatrixHistogramComponent: React.FC (title != null && typeof title === 'function' ? title(selectedStackByOption) : title), [title, selectedStackByOption] ); - const subtitleWithCounts = useMemo( - () => (subtitle != null && typeof subtitle === 'function' ? subtitle(totalCount) : subtitle), - [subtitle, totalCount] - ); + const subtitleWithCounts = useMemo(() => { + if (isInitialLoading) { + return null; + } + + if (typeof subtitle === 'function') { + return totalCount >= 0 ? subtitle(totalCount) : null; + } + + return subtitle; + }, [isInitialLoading, subtitle, totalCount]); const hideHistogram = useMemo(() => (totalCount <= 0 && hideHistogramIfEmpty ? true : false), [ totalCount, hideHistogramIfEmpty, @@ -155,7 +176,9 @@ export const MatrixHistogramComponent: React.FC getCustomChartData(data, mapping), [data, mapping]); useEffect(() => { - setQuery({ id, inspect, loading, refetch }); + if (!loading && !isInitialLoading) { + setQuery({ id, inspect, loading, refetch }); + } if (isInitialLoading && !!barChartData && data) { setIsInitialLoading(false); @@ -189,59 +212,39 @@ export const MatrixHistogramComponent: React.FC )} + + + + {stackByOptions.length > 1 && ( + + )} + + {headerChildren} + + + {isInitialLoading ? ( - <> - = 0 ? subtitleWithCounts : null)} - > - - - {stackByOptions?.length > 1 && ( - - )} - - {headerChildren} - - - - + ) : ( - <> - = 0 ? subtitleWithCounts : null) - } - > - - - {stackByOptions?.length > 1 && ( - - )} - - {headerChildren} - - - - + )} - + {showSpacer && } ); }; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts index a435c7be6c890..c59775ad325d0 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiTitleSize } from '@elastic/eui'; import { ScaleType, Position, TickFormatter } from '@elastic/charts'; import { ActionCreator } from 'redux'; import { ESQuery } from '../../../common/typed_json'; @@ -34,6 +35,7 @@ export interface MatrixHisrogramConfigs { stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; title: string | GetTitle; + titleSize?: EuiTitleSize; } interface MatrixHistogramBasicProps { @@ -57,14 +59,22 @@ interface MatrixHistogramBasicProps { stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; title?: string | GetTitle; + titleSize?: EuiTitleSize; } export interface MatrixHistogramQueryProps { endDate: number; errorMessage: string; filterQuery?: ESQuery | string | undefined; + setAbsoluteRangeDatePicker?: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + setAbsoluteRangeDatePickerTarget?: InputsModelId; stackByField: string; startDate: number; + indexToAdd?: string[] | null; isInspected: boolean; histogramType: HistogramType; } @@ -73,6 +83,7 @@ export interface MatrixHistogramProps extends MatrixHistogramBasicProps { scaleType?: ScaleType; yTickFormatter?: (value: number) => string; showLegend?: boolean; + showSpacer?: boolean; legendPosition?: Position; } diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts index ddac615cef50a..d31eb1da15ea1 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts @@ -69,6 +69,19 @@ export const getBarchartConfigs = ({ customHeight: chartHeight ?? DEFAULT_CHART_HEIGHT, }); +export const defaultLegendColors = [ + '#1EA593', + '#2B70F7', + '#CE0060', + '#38007E', + '#FCA5D3', + '#F37020', + '#E49E29', + '#B0916F', + '#7B000B', + '#34130C', +]; + export const formatToChartDataItem = ([key, value]: [ string, MatrixOverTimeHistogramData[] diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx index 11761c8fd39b0..4463f8d4ff602 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx @@ -5,7 +5,7 @@ */ import { EuiPanel, EuiToolTip } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { WithCopyToClipboard } from '../../../lib/clipboard/with_copy_to_clipboard'; @@ -19,33 +19,23 @@ const BodyContainer = styled(EuiPanel)` BodyContainer.displayName = 'BodyContainer'; -const HoverActionsContainer = styled(EuiPanel)` - align-items: center; - display: flex; - flex-direction: row; - height: 25px; - justify-content: center; - left: 5px; - position: absolute; - top: -5px; - width: 30px; -`; - -HoverActionsContainer.displayName = 'HoverActionsContainer'; - -export const NoteCardBody = React.memo<{ rawNote: string }>(({ rawNote }) => ( - - - - - - - } - render={() => } - /> - -)); +export const NoteCardBody = React.memo<{ rawNote: string }>(({ rawNote }) => { + const hoverContent = useMemo( + () => ( + + + + ), + [rawNote] + ); + + const render = useCallback(() => , [rawNote]); + + return ( + + + + ); +}); NoteCardBody.displayName = 'NoteCardBody'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap index 42ef4e5404faa..ef02311c0629e 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap @@ -3,22 +3,36 @@ exports[`AddFilterToGlobalSearchBar Component Rendering 1`] = ` - - + + + + } render={[Function]} /> diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx index 7e5e53f575be8..5c920d923d9a4 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx @@ -24,6 +24,22 @@ describe('helpers', () => { }); }); + test('returns a negated filter when `negate` is true', () => { + const filter = createFilter('host.name', 'siem-xavier', true); + expect(filter).toEqual({ + meta: { + alias: null, + disabled: false, + key: 'host.name', + negate: true, // <-- filter is negated + params: { query: 'siem-xavier' }, + type: 'phrase', + value: 'siem-xavier', + }, + query: { match: { 'host.name': { query: 'siem-xavier', type: 'phrase' } } }, + }); + }); + test('return valid exists filter when valid key and null value are provided', () => { const filter = createFilter('host.name', null); expect(filter).toEqual({ diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts index bafe033368c83..d88bc2bf3b7e6 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts @@ -6,13 +6,17 @@ import { Filter } from '../../../../../../../../src/plugins/data/public'; -export const createFilter = (key: string, value: string[] | string | null | undefined): Filter => { +export const createFilter = ( + key: string, + value: string[] | string | null | undefined, + negate: boolean = false +): Filter => { const queryValue = value != null ? (Array.isArray(value) ? value[0] : value) : null; return queryValue != null ? { meta: { alias: null, - negate: false, + negate, disabled: false, type: 'phrase', key, diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx index 160cd020796db..127eb3bae0284 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { useCallback } from 'react'; -import styled from 'styled-components'; import { Filter } from '../../../../../../../../src/plugins/data/public'; import { WithHoverActions } from '../../with_hover_actions'; @@ -26,21 +25,52 @@ export const AddFilterToGlobalSearchBar = React.memo( ({ children, filter, onFilterAdded }) => { const { filterManager } = useKibana().services.data.query; - const addToKql = useCallback(() => { + const filterForValue = useCallback(() => { filterManager.addFilters(filter); + if (onFilterAdded != null) { onFilterAdded(); } - }, [filter, filterManager, onFilterAdded]); + }, [filterManager, filter, onFilterAdded]); + + const filterOutValue = useCallback(() => { + filterManager.addFilters({ + ...filter, + meta: { + ...filter.meta, + negate: true, + }, + }); + + if (onFilterAdded != null) { + onFilterAdded(); + } + }, [filterManager, filter, onFilterAdded]); return ( +
- + + + + + - +
} render={() => children} /> @@ -49,16 +79,3 @@ export const AddFilterToGlobalSearchBar = React.memo( ); AddFilterToGlobalSearchBar.displayName = 'AddFilterToGlobalSearchBar'; - -export const HoverActionsContainer = styled(EuiPanel)` - align-items: center; - display: flex; - flex-direction: row; - height: 34px; - justify-content: center; - left: 5px; - position: absolute; - top: -10px; - width: 34px; - cursor: pointer; -`; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts index 81772527e59db..f192c5c26fa49 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts @@ -12,3 +12,10 @@ export const FILTER_FOR_VALUE = i18n.translate( defaultMessage: 'Filter for value', } ); + +export const FILTER_OUT_VALUE = i18n.translate( + 'xpack.siem.add_filter_to_global_search_bar.filterOutValueHoverAction', + { + defaultMessage: 'Filter out value', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx index 8d490d2c152d9..6bd82f3192f9b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx @@ -46,9 +46,7 @@ export const getHostsColumns = (): HostsTableColumns => [ ) : ( - - - + ) } /> diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index ef6a19f4b7448..3a36a2dce476b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -17,7 +17,7 @@ export const AppGlobalStyle = createGlobalStyle` position: static; } /* end of dirty hack to fix draggables with tooltip on FF */ - + div.app-wrapper { background-color: rgba(0,0,0,0); } @@ -28,12 +28,13 @@ export const AppGlobalStyle = createGlobalStyle` .euiPopover__panel.euiPopover__panel-isOpen { z-index: 9900 !important; + min-width: 24px; } .euiToolTip { z-index: 9950 !important; } - /* + /* overrides the default styling of euiComboBoxOptionsList because it's implemented as a popover, so it's not selectable as a child of the styled component */ @@ -45,6 +46,17 @@ export const AppGlobalStyle = createGlobalStyle` .euiPanel-loading-hide-border { border: none; } + + /* hide open popovers when a modal is being displayed to prevent them from covering the modal */ + body.euiBody-hasOverlayMask .euiPopover__panel-isOpen { + visibility: hidden !important; + } + + /* ensure elastic charts tooltips appear above open euiPopovers */ + .echTooltip { + z-index: 9950; + } + `; export const DescriptionListStyled = styled(EuiDescriptionList)` diff --git a/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx b/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx index 138c38c02065b..3b723c66f5af5 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx @@ -42,6 +42,6 @@ export function manageQuery(WrappedComponent: React.ComponentClass | React return ; } } - ManageQuery.displayName = `ManageQuery (${WrappedComponent.displayName || 'Unknown'})`; + ManageQuery.displayName = `ManageQuery (${WrappedComponent?.displayName ?? 'Unknown'})`; return ManageQuery; } diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx index 5cd200cbb41b7..73c3d2da184e7 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { EuiBasicTable, EuiBasicTableProps, @@ -246,21 +247,16 @@ const PaginatedTableComponent: FC = ({ ) : ( <> - { - // @ts-ignore avoid some type mismatches - } = ({ export const PaginatedTable = memo(PaginatedTableComponent); type BasicTableType = ComponentType>; // eslint-disable-line @typescript-eslint/no-explicit-any -const BasicTable: typeof EuiBasicTable & { displayName: string } = styled( - EuiBasicTable as BasicTableType -)` +const BasicTable = styled(EuiBasicTable as BasicTableType)` tbody { th, td { diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx index b8192cce11e5a..62f01dfc020f5 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx @@ -171,7 +171,7 @@ export const SourceDestinationIp = React.memo( return isIpFieldPopulated({ destinationIp, sourceIp, type }) || hasPorts({ destinationPort, sourcePort, type }) ? ( - + (({ header, onColumnRemoved, sort }) => { - const isLoading = useTimelineContext(); + const { isLoading } = useTimelineContext(); return ( <> {sort.columnId === header.id && isLoading ? ( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx index 84781e6a24300..0a69cef618570 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx @@ -32,7 +32,7 @@ const HeaderContentComponent: React.FC = ({ onClick, sort, }) => { - const isLoading = useTimelineContext(); + const { isLoading } = useTimelineContext(); return ( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index 5f59915eac418..417a078a08150 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -19,6 +19,8 @@ import { createGenericFileRowRenderer, } from './generic_row_renderer'; +jest.mock('../../../../../pages/overview/events_by_dataset'); + describe('GenericRowRenderer', () => { const mount = useMountAppended(); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index 9ccd1fb7a0519..24c52f3372d62 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -44,7 +44,7 @@ export const Tokens = React.memo<{ tokens: string[] }>(({ tokens }) => ( <> {tokens.map(token => ( - + {token} @@ -81,7 +81,7 @@ export const DraggableSignatureId = React.memo<{ id: string; signatureId: number data-test-subj="signature-id-tooltip" content={SURICATA_SIGNATURE_ID_FIELD_NAME} > - + {signatureId} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx index e1524c8e5aecb..2ad3eb4681454 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx @@ -119,7 +119,7 @@ export const SystemGenericLine = React.memo( - + diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx index c47d9603cbea2..ef7c3b3ccf859 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx @@ -193,7 +193,7 @@ export const SystemGenericFileLine = React.memo( - + diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index abe77a63f4a27..2f5fa76855f2b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -48,6 +48,8 @@ import { } from './generic_row_renderer'; import * as i18n from './translations'; +jest.mock('../../../../../pages/overview/events_by_dataset'); + describe('GenericRowRenderer', () => { const mount = useMountAppended(); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index f13a236e8ec36..39c21c4ffa33b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -92,7 +92,7 @@ export const DraggableZeekElement = React.memo<{ ) : ( - + {stringRenderer(value)} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx index 525cc8e301d11..f369b961807af 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx @@ -109,7 +109,7 @@ export const DataProviders = React.memo( data-test-subj="dataProviders" > - {isLoading => ( + {({ isLoading }) => ( {dataProviders != null && dataProviders.length ? ( ( ({ deleteProvider, field, isEnabled, isExcluded, operator, providerId, togglePopover, val }) => { - const deleteFilter: React.MouseEventHandler = ( - event: React.MouseEvent - ) => { - // Make sure it doesn't also trigger the onclick for the whole badge - if (event.stopPropagation) { - event.stopPropagation(); - } - deleteProvider(); - }; - const classes = classNames('globalFilterItem', { - 'globalFilterItem-isDisabled': !isEnabled, - 'globalFilterItem-isExcluded': isExcluded, - }); - const formattedValue = isString(val) && val === '' ? getEmptyString() : val; - const prefix = isExcluded ? {i18n.NOT} : null; - const title = `${field}: "${formattedValue}"`; - - return ( - - {prefix} - {operator !== EXISTS_OPERATOR ? ( - <> - {`${field}: `} - {`"${formattedValue}"`} - - ) : ( - - {field} {i18n.EXISTS_LABEL} - - )} - + const deleteFilter: React.MouseEventHandler = useCallback( + (event: React.MouseEvent) => { + // Make sure it doesn't also trigger the onclick for the whole badge + if (event.stopPropagation) { + event.stopPropagation(); + } + deleteProvider(); + }, + [deleteProvider] + ); + + const classes = useMemo( + () => + classNames('globalFilterItem', { + 'globalFilterItem-isDisabled': !isEnabled, + 'globalFilterItem-isExcluded': isExcluded, + }), + [isEnabled, isExcluded] + ); + + const formattedValue = useMemo(() => (isString(val) && val === '' ? getEmptyString() : val), [ + val, + ]); + + const prefix = useMemo(() => (isExcluded ? {i18n.NOT} : null), [isExcluded]); + + const title = useMemo(() => `${field}: "${formattedValue}"`, [field, formattedValue]); + + const hoverContent = useMemo( + () => ( + + ), + [field, val] ); + + const badge = useCallback( + () => ( + + {prefix} + {operator !== EXISTS_OPERATOR ? ( + <> + {`${field}: `} + {`"${formattedValue}"`} + + ) : ( + + {field} {i18n.EXISTS_LABEL} + + )} + + ), + [ + providerId, + field, + val, + classes, + title, + deleteFilter, + togglePopover, + formattedValue, + closeButtonProps, + prefix, + operator, + ] + ); + + return ; } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx index 79f9c32a176f5..2cc19537d6a63 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx @@ -71,7 +71,7 @@ export const ProviderItemBadge = React.memo( return ( - {isLoading => ( + {({ isLoading }) => ( { - const mockTimelineContext: boolean = true; + const isLoading: boolean = true; const mount = useMountAppended(); + const filterManager = new FilterManager(mockUiSettingsForFilterManager); describe('rendering', () => { test('renders correctly against snapshot', () => { @@ -96,7 +101,7 @@ describe('Providers', () => { const mockOnDataProviderRemoved = jest.fn(); const wrapper = mount( - + { const mockOnDataProviderRemoved = jest.fn(); const wrapper = mount( - + { const mockOnToggleDataProviderEnabled = jest.fn(); const wrapper = mount( - + { const wrapper = mount( - + { const wrapper = mount( - + { const wrapper = mount( - + { const wrapper = mount( - + { @@ -26,6 +30,7 @@ describe('Header', () => { { { = ({ id, indexPattern, dataProviders, + filterManager, onChangeDataProviderKqlQuery, onChangeDroppableAndProvider, onDataProviderEdited, @@ -77,6 +79,7 @@ const TimelineHeaderComponent: React.FC = ({ /> @@ -90,6 +93,7 @@ export const TimelineHeader = React.memo( prevProps.id === nextProps.id && deepEqual(prevProps.indexPattern, nextProps.indexPattern) && deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + prevProps.filterManager === nextProps.filterManager && prevProps.onChangeDataProviderKqlQuery === nextProps.onChangeDataProviderKqlQuery && prevProps.onChangeDroppableAndProvider === nextProps.onChangeDroppableAndProvider && prevProps.onDataProviderEdited === nextProps.onDataProviderEdited && diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx index b978ef3d478d8..943133dc2063c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx @@ -11,12 +11,16 @@ import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; import { mockBrowserFields } from '../../../containers/source/mock'; import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; import { mockIndexPattern, TestProviders } from '../../../mock'; +import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; import { QueryBar } from '../../query_bar'; +import { FilterManager } from '../../../../../../../../src/plugins/data/public'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { buildGlobalQuery } from '../helpers'; import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + jest.mock('../../../lib/kibana'); describe('Timeline QueryBar ', () => { @@ -58,6 +62,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} @@ -99,6 +104,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} @@ -145,6 +151,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} @@ -189,6 +196,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} @@ -235,6 +243,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} @@ -279,6 +288,7 @@ describe('Timeline QueryBar ', () => { browserFields={mockBrowserFields} dataProviders={mockDataProviders} filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} filterQuery={{ expression: 'here: query', kind: 'kuery' }} filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} from={0} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx index 7f662cdb2f1b4..f53f7bb56e2f4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -21,7 +21,6 @@ import { import { BrowserFields } from '../../../containers/source'; import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { useKibana } from '../../../lib/kibana'; import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; import { KqlMode } from '../../../store/timeline/model'; import { useSavedQueryServices } from '../../../utils/saved_query_services'; @@ -35,6 +34,7 @@ export interface QueryBarTimelineComponentProps { browserFields: BrowserFields; dataProviders: DataProvider[]; filters: Filter[]; + filterManager: FilterManager; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; from: number; @@ -61,6 +61,7 @@ export const QueryBarTimeline = memo( browserFields, dataProviders, filters, + filterManager, filterQuery, filterQueryDraft, from, @@ -94,9 +95,6 @@ export const QueryBarTimeline = memo( const [dataProvidersDsl, setDataProvidersDsl] = useState( convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) ); - const kibana = useKibana(); - const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - const savedQueryServices = useSavedQueryServices(); useEffect(() => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index 87061bdbb5d02..a0a0ac4c2b85c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -10,7 +10,11 @@ import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; import deepEqual from 'fast-deep-equal'; -import { Filter, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { + Filter, + FilterManager, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../containers/source'; import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; import { @@ -29,6 +33,7 @@ import { SearchOrFilter } from './search_or_filter'; interface OwnProps { browserFields: BrowserFields; + filterManager: FilterManager; indexPattern: IIndexPattern; timelineId: string; } @@ -42,6 +47,7 @@ const StatefulSearchOrFilterComponent = React.memo( dataProviders, eventType, filters, + filterManager, filterQuery, filterQueryDraft, from, @@ -122,6 +128,7 @@ const StatefulSearchOrFilterComponent = React.memo( dataProviders={dataProviders} eventType={eventType} filters={filters} + filterManager={filterManager} filterQuery={filterQuery} filterQueryDraft={filterQueryDraft} from={from} @@ -146,6 +153,7 @@ const StatefulSearchOrFilterComponent = React.memo( (prevProps, nextProps) => { return ( prevProps.eventType === nextProps.eventType && + prevProps.filterManager === nextProps.filterManager && prevProps.from === nextProps.from && prevProps.fromStr === nextProps.fromStr && prevProps.to === nextProps.to && diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 7bdd92e745f21..02a575db259bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -8,7 +8,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/ import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; -import { Filter, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { + Filter, + FilterManager, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../containers/source'; import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; import { KqlMode, EventType } from '../../../store/timeline/model'; @@ -44,6 +48,7 @@ interface Props { browserFields: BrowserFields; dataProviders: DataProvider[]; eventType: EventType; + filterManager: FilterManager; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; from: number; @@ -95,6 +100,7 @@ export const SearchOrFilter = React.memo( indexPattern, isRefreshPaused, filters, + filterManager, filterQuery, filterQueryDraft, from, @@ -135,6 +141,7 @@ export const SearchOrFilter = React.memo( browserFields={browserFields} dataProviders={dataProviders} filters={filters} + filterManager={filterManager} filterQuery={filterQuery} filterQueryDraft={filterQueryDraft} from={from} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index 098dd82791610..222cc0530bddb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -6,7 +6,7 @@ import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; -import React, { useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import styled from 'styled-components'; import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; @@ -34,7 +34,12 @@ import { TimelineHeader } from './header'; import { combineQueries } from './helpers'; import { TimelineRefetch } from './refetch_timeline'; import { ManageTimelineContext } from './timeline_context'; -import { esQuery, Filter, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { + esQuery, + Filter, + FilterManager, + IIndexPattern, +} from '../../../../../../../src/plugins/data/public'; const TimelineContainer = styled.div` height: 100%; @@ -143,6 +148,7 @@ export const TimelineComponent: React.FC = ({ usersViewing, }) => { const kibana = useKibana(); + const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), dataProviders, @@ -178,6 +184,7 @@ export const TimelineComponent: React.FC = ({ id={id} indexPattern={indexPattern} dataProviders={dataProviders} + filterManager={filterManager} onChangeDataProviderKqlQuery={onChangeDataProviderKqlQuery} onChangeDroppableAndProvider={onChangeDroppableAndProvider} onDataProviderEdited={onDataProviderEdited} @@ -211,7 +218,12 @@ export const TimelineComponent: React.FC = ({ getUpdatedAt, refetch, }) => ( - + (initTimelineContext); +interface TimelineContextState { + filterManager: FilterManager | undefined; + isLoading: boolean; +} + +const initTimelineContext: TimelineContextState = { filterManager: undefined, isLoading: false }; +export const TimelineContext = createContext(initTimelineContext); export const useTimelineContext = () => useContext(TimelineContext); export interface TimelineTypeContextProps { documentType?: string; footerText?: string; + id?: string; + indexToAdd?: string[] | null; loadingText?: string; queryFields?: string[]; selectAll?: boolean; @@ -24,6 +34,8 @@ export interface TimelineTypeContextProps { const initTimelineType: TimelineTypeContextProps = { documentType: undefined, footerText: undefined, + id: undefined, + indexToAdd: undefined, loadingText: undefined, queryFields: [], selectAll: false, @@ -36,6 +48,8 @@ export const useTimelineTypeContext = () => useContext(TimelineTypeContext); interface ManageTimelineContextProps { children: React.ReactNode; + filterManager: FilterManager; + indexToAdd?: string[] | null; loading: boolean; type?: TimelineTypeContextProps; } @@ -44,22 +58,27 @@ interface ManageTimelineContextProps { // to avoid so many Context, at least the separation of code is there now const ManageTimelineContextComponent: React.FC = ({ children, + filterManager, + indexToAdd, loading, - type = initTimelineType, + type = { ...initTimelineType, indexToAdd }, }) => { - const [myLoading, setLoading] = useState(initTimelineContext); - const [myType, setType] = useState(initTimelineType); + const [myContextState, setMyContextState] = useState({ + filterManager, + isLoading: false, + }); + const [myType, setType] = useState(type); useEffect(() => { - setLoading(loading); - }, [loading]); + setMyContextState({ filterManager, isLoading: loading }); + }, [setMyContextState, filterManager, loading]); useEffect(() => { - setType(type); - }, [type]); + setType({ ...type, indexToAdd }); + }, [type, indexToAdd]); return ( - + {children} ); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/helpers.test.tsx new file mode 100644 index 0000000000000..da0f6f59b533f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/helpers.test.tsx @@ -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 { allEvents, defaultOptions, getOptions, rawEvents, signalEvents } from './helpers'; + +describe('getOptions', () => { + test(`it returns the default options when 'activeTimelineEventType' is undefined`, () => { + expect(getOptions()).toEqual(defaultOptions); + }); + + test(`it returns 'allEvents' when 'activeTimelineEventType' is 'all'`, () => { + expect(getOptions('all')).toEqual(allEvents); + }); + + test(`it returns 'rawEvents' when 'activeTimelineEventType' is 'raw'`, () => { + expect(getOptions('raw')).toEqual(rawEvents); + }); + + test(`it returns 'signalEvents' when 'activeTimelineEventType' is 'signal'`, () => { + expect(getOptions('signal')).toEqual(signalEvents); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/helpers.ts b/x-pack/legacy/plugins/siem/public/components/top_n/helpers.ts new file mode 100644 index 0000000000000..8d9ae48d29b69 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/helpers.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EventType } from '../../store/timeline/model'; + +import * as i18n from './translations'; + +export interface TopNOption { + inputDisplay: string; + value: EventType; + 'data-test-subj': string; +} + +/** A (stable) array containing only the 'All events' option */ +export const allEvents: TopNOption[] = [ + { + value: 'all', + inputDisplay: i18n.ALL_EVENTS, + 'data-test-subj': 'option-all', + }, +]; + +/** A (stable) array containing only the 'Raw events' option */ +export const rawEvents: TopNOption[] = [ + { + value: 'raw', + inputDisplay: i18n.RAW_EVENTS, + 'data-test-subj': 'option-raw', + }, +]; + +/** A (stable) array containing only the 'Signal events' option */ +export const signalEvents: TopNOption[] = [ + { + value: 'signal', + inputDisplay: i18n.SIGNAL_EVENTS, + 'data-test-subj': 'option-signal', + }, +]; + +/** A (stable) array containing the default Top N options */ +export const defaultOptions = [...rawEvents, ...signalEvents]; + +/** + * Returns the options to be displayed in a Top N view select. When + * an `activeTimelineEventType` is provided, an array containing + * just one option (corresponding to `activeTimelineEventType`) + * will be returned, to ensure the data displayed in the Top N + * is always in sync with the `EventType` chosen by the user in + * the active timeline. + */ +export const getOptions = (activeTimelineEventType?: EventType): TopNOption[] => { + switch (activeTimelineEventType) { + case 'all': + return allEvents; + case 'raw': + return rawEvents; + case 'signal': + return signalEvents; + default: + return defaultOptions; + } +}; diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/index.test.tsx new file mode 100644 index 0000000000000..61772f1dc7a7a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/index.test.tsx @@ -0,0 +1,379 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; + +import { mockBrowserFields } from '../../containers/source/mock'; +import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mock'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { createStore, State } from '../../store'; +import { TimelineContext, TimelineTypeContext } from '../timeline/timeline_context'; + +import { Props } from './top_n'; +import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; + +jest.mock('../../lib/kibana'); + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +const field = 'process.name'; +const value = 'nice'; + +const state: State = { + ...mockGlobalState, + inputs: { + ...mockGlobalState.inputs, + global: { + ...mockGlobalState.inputs.global, + query: { + query: 'host.name : end*', + language: 'kuery', + }, + filters: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.os.name', + params: { + query: 'Linux', + }, + }, + query: { + match: { + 'host.os.name': { + query: 'Linux', + type: 'phrase', + }, + }, + }, + }, + ], + }, + timeline: { + ...mockGlobalState.inputs.timeline, + timerange: { + kind: 'relative', + fromStr: 'now-24h', + toStr: 'now', + from: 1586835969047, + to: 1586922369047, + }, + }, + }, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + [ACTIVE_TIMELINE_REDUX_ID]: { + ...mockGlobalState.timeline.timelineById.test, + id: ACTIVE_TIMELINE_REDUX_ID, + dataProviders: [ + { + id: + 'draggable-badge-default-draggable-netflow-renderer-timeline-1-_qpBe3EBD7k-aQQL7v7--_qpBe3EBD7k-aQQL7v7--network_transport-tcp', + name: 'tcp', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'network.transport', + value: 'tcp', + operator: ':', + }, + and: [], + }, + ], + eventType: 'all', + filters: [ + { + meta: { + alias: null, + disabled: false, + key: 'source.port', + negate: false, + params: { + query: '30045', + }, + type: 'phrase', + }, + query: { + match: { + 'source.port': { + query: '30045', + type: 'phrase', + }, + }, + }, + }, + ], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: 'host.name : *', + }, + serializedQuery: + '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', + }, + filterQueryDraft: { + kind: 'kuery', + expression: 'host.name : *', + }, + }, + }, + }, + }, +}; +const store = createStore(state, apolloClientObservable); + +describe('StatefulTopN', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + describe('rendering in a global NON-timeline context', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + + ); + }); + + test('it has undefined combinedQueries when rendering in a global context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.combinedQueries).toBeUndefined(); + }); + + test(`defaults to the 'Raw events' view when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('raw'); + }); + + test(`provides a 'deleteQuery' when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.deleteQuery).toBeDefined(); + }); + + test(`provides filters from Redux state (inputs > global > filters) when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.filters).toEqual([ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.os.name', + params: { query: 'Linux' }, + }, + query: { match: { 'host.os.name': { query: 'Linux', type: 'phrase' } } }, + }, + ]); + }); + + test(`provides 'from' via GlobalTime when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.from).toEqual(0); + }); + + test('provides the global query from Redux state (inputs > global > query) when rendering in a global context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.query).toEqual({ query: 'host.name : end*', language: 'kuery' }); + }); + + test(`provides a 'global' 'setAbsoluteRangeDatePickerTarget' when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.setAbsoluteRangeDatePickerTarget).toEqual('global'); + }); + + test(`provides 'to' via GlobalTime when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.to).toEqual(1); + }); + }); + + describe('rendering in a timeline context', () => { + let filterManager: FilterManager; + let wrapper: ReactWrapper; + + beforeEach(() => { + filterManager = new FilterManager(mockUiSettingsForFilterManager); + + wrapper = mount( + + + + + + + + ); + }); + + test('it has a combinedQueries value from Redux state composed of the timeline [data providers + kql + filter-bar-filters] when rendering in a timeline context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.combinedQueries).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1586835969047}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1586922369047}}}],"minimum_should_match":1}}]}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' + ); + }); + + test('it provides only one view option that matches the `eventType` from redux when rendering in the context of the active timeline', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('all'); + }); + + test(`provides an undefined 'deleteQuery' when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.deleteQuery).toBeUndefined(); + }); + + test(`provides empty filters when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.filters).toEqual([]); + }); + + test(`provides 'from' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.from).toEqual(1586835969047); + }); + + test('provides an empty query when rendering in a timeline context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.query).toEqual({ query: '', language: 'kuery' }); + }); + + test(`provides a 'timeline' 'setAbsoluteRangeDatePickerTarget' when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.setAbsoluteRangeDatePickerTarget).toEqual('timeline'); + }); + + test(`provides 'to' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.to).toEqual(1586922369047); + }); + }); + + test(`defaults to the 'Signals events' option when rendering in a NON-active timeline context (e.g. the Signals table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'signals'`, () => { + const filterManager = new FilterManager(mockUiSettingsForFilterManager); + const wrapper = mount( + + + + + + + + ); + + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('signal'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx new file mode 100644 index 0000000000000..983b234a04eaa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { GlobalTime } from '../../containers/global_time'; +import { BrowserFields, WithSource } from '../../containers/source'; +import { useKibana } from '../../lib/kibana'; +import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/public'; +import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { timelineDefaults } from '../../store/timeline/defaults'; +import { TimelineModel } from '../../store/timeline/model'; +import { combineQueries } from '../timeline/helpers'; +import { useTimelineTypeContext } from '../timeline/timeline_context'; + +import { getOptions } from './helpers'; +import { TopN } from './top_n'; + +/** The currently active timeline always has this Redux ID */ +export const ACTIVE_TIMELINE_REDUX_ID = 'timeline-1'; + +const EMPTY_FILTERS: Filter[] = []; +const EMPTY_QUERY: Query = { query: '', language: 'kuery' }; + +const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); + + // The mapped Redux state provided to this component includes the global + // filters that appear at the top of most views in the app, and all the + // filters in the active timeline: + const mapStateToProps = (state: State) => { + const activeTimeline: TimelineModel = + getTimeline(state, ACTIVE_TIMELINE_REDUX_ID) ?? timelineDefaults; + const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; + const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); + + return { + activeTimelineEventType: activeTimeline.eventType, + activeTimelineFilters, + activeTimelineFrom: activeTimelineInput.timerange.from, + activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, ACTIVE_TIMELINE_REDUX_ID), + activeTimelineTo: activeTimelineInput.timerange.to, + dataProviders: activeTimeline.dataProviders, + globalQuery: getGlobalQuerySelector(state), + globalFilters: getGlobalFiltersQuerySelector(state), + kqlMode: activeTimeline.kqlMode, + }; + }; + + return mapStateToProps; +}; + +const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +interface OwnProps { + browserFields: BrowserFields; + field: string; + toggleTopN: () => void; + onFilterAdded?: () => void; + value?: string[] | string | null; +} +type PropsFromRedux = ConnectedProps; +type Props = OwnProps & PropsFromRedux; + +const StatefulTopNComponent: React.FC = ({ + activeTimelineEventType, + activeTimelineFilters, + activeTimelineFrom, + activeTimelineKqlQueryExpression, + activeTimelineTo, + browserFields, + dataProviders, + field, + globalFilters = EMPTY_FILTERS, + globalQuery = EMPTY_QUERY, + kqlMode, + onFilterAdded, + setAbsoluteRangeDatePicker, + toggleTopN, + value, +}) => { + const kibana = useKibana(); + + // Regarding data from useTimelineTypeContext: + // * `documentType` (e.g. 'signals') may only be populated in some views, + // e.g. the `Signals` view on the `Detections` page. + // * `id` (`timelineId`) may only be populated when we are rendered in the + // context of the active timeline. + // * `indexToAdd`, which enables the signals index to be appended to + // the `indexPattern` returned by `WithSource`, may only be populated when + // this component is rendered in the context of the active timeline. This + // behavior enables the 'All events' view by appending the signals index + // to the index pattern. + const { documentType, id: timelineId, indexToAdd } = useTimelineTypeContext(); + + const options = getOptions( + timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined + ); + + return ( + + {({ from, deleteQuery, setQuery, to }) => ( + + {({ indexPattern }) => ( + + )} + + )} + + ); +}; + +StatefulTopNComponent.displayName = 'StatefulTopNComponent'; + +export const StatefulTopN = connector(React.memo(StatefulTopNComponent)); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/top_n.test.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/top_n.test.tsx new file mode 100644 index 0000000000000..13b77ea0ccd4c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/top_n.test.tsx @@ -0,0 +1,261 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; + +import { TestProviders, mockIndexPattern } from '../../mock'; +import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; + +import { allEvents, defaultOptions } from './helpers'; +import { TopN } from './top_n'; + +jest.mock('../../lib/kibana'); + +jest.mock('uuid', () => { + return { + v1: jest.fn(() => 'uuid.v1()'), + v4: jest.fn(() => 'uuid.v4()'), + }; +}); + +const field = 'process.name'; +const value = 'nice'; +const combinedQueries = { + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [{ match_phrase: { 'network.transport': 'tcp' } }], + minimum_should_match: 1, + }, + }, + { + bool: { should: [{ exists: { field: 'host.name' } }], minimum_should_match: 1 }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [{ range: { '@timestamp': { gte: 1586824450493 } } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ range: { '@timestamp': { lte: 1586910850493 } } }], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + }, + }, + { match_phrase: { 'source.port': { query: '30045' } } }, + ], + should: [], + must_not: [], + }, +}; + +describe('TopN', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + const query = { query: '', language: 'kuery' }; + + describe('common functionality', () => { + let toggleTopN: () => void; + let wrapper: ReactWrapper; + + beforeEach(() => { + toggleTopN = jest.fn(); + wrapper = mount( + + + + ); + }); + + test('it invokes the toggleTopN function when the close button is clicked', () => { + wrapper + .find('[data-test-subj="close"]') + .first() + .simulate('click'); + wrapper.update(); + + expect(toggleTopN).toHaveBeenCalled(); + }); + + test('it enables the view select by default', () => { + expect( + wrapper + .find('[data-test-subj="view-select"]') + .first() + .props().disabled + ).toBe(false); + }); + }); + + describe('events view', () => { + let toggleTopN: () => void; + let wrapper: ReactWrapper; + + beforeEach(() => { + toggleTopN = jest.fn(); + wrapper = mount( + + + + ); + }); + + test(`it renders EventsByDataset when defaultView is 'raw'`, () => { + expect( + wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists() + ).toBe(true); + }); + + test(`it does NOT render SignalsByCategory when defaultView is 'raw'`, () => { + expect(wrapper.find('[data-test-subj="signals-histogram-panel"]').exists()).toBe(false); + }); + }); + + describe('signals view', () => { + let toggleTopN: () => void; + let wrapper: ReactWrapper; + + beforeEach(() => { + toggleTopN = jest.fn(); + wrapper = mount( + + + + ); + }); + + test(`it renders SignalsByCategory when defaultView is 'signal'`, () => { + expect(wrapper.find('[data-test-subj="signals-histogram-panel"]').exists()).toBe(true); + }); + + test(`it does NOT render EventsByDataset when defaultView is 'signal'`, () => { + expect( + wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists() + ).toBe(false); + }); + }); + + describe('All events, a view shown only when rendered in the context of the active timeline', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + + ); + }); + + test(`it disables the view select when 'options' contains only one entry`, () => { + expect( + wrapper + .find('[data-test-subj="view-select"]') + .first() + .props().disabled + ).toBe(true); + }); + + test(`it renders EventsByDataset when defaultView is 'all'`, () => { + expect( + wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists() + ).toBe(true); + }); + + test(`it does NOT render SignalsByCategory when defaultView is 'all'`, () => { + expect(wrapper.find('[data-test-subj="signals-histogram-panel"]').exists()).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx new file mode 100644 index 0000000000000..136252617e2a2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx @@ -0,0 +1,158 @@ +/* + * 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 { EuiButtonIcon, EuiSuperSelect } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { ActionCreator } from 'typescript-fsa'; + +import { EventsByDataset } from '../../pages/overview/events_by_dataset'; +import { SignalsByCategory } from '../../pages/overview/signals_by_category'; +import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../store'; +import { InputsModelId } from '../../store/inputs/constants'; +import { EventType } from '../../store/timeline/model'; + +import { TopNOption } from './helpers'; +import * as i18n from './translations'; + +const TopNContainer = styled.div` + width: 600px; +`; + +const CloseButton = styled(EuiButtonIcon)` + z-index: 999999; + position: absolute; + right: 4px; + top: 4px; +`; + +const ViewSelect = styled(EuiSuperSelect)` + z-index: 999999; + width: 155px; +`; + +const TopNContent = styled.div` + margin-top: 4px; + + .euiPanel { + border: none; + } +`; + +export interface Props { + combinedQueries?: string; + defaultView: EventType; + deleteQuery?: ({ id }: { id: string }) => void; + field: string; + filters: Filter[]; + from: number; + indexPattern: IIndexPattern; + indexToAdd?: string[] | null; + options: TopNOption[]; + query: Query; + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + setAbsoluteRangeDatePickerTarget: InputsModelId; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; + toggleTopN: () => void; + onFilterAdded?: () => void; + value?: string[] | string | null; +} + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; + +const TopNComponent: React.FC = ({ + combinedQueries, + defaultView, + deleteQuery, + filters = NO_FILTERS, + field, + from, + indexPattern, + indexToAdd, + options, + query = DEFAULT_QUERY, + setAbsoluteRangeDatePicker, + setAbsoluteRangeDatePickerTarget, + setQuery, + to, + toggleTopN, +}) => { + const [view, setView] = useState(defaultView); + const onViewSelected = useCallback((value: string) => setView(value as EventType), [setView]); + + const headerChildren = useMemo( + () => ( + + ), + [onViewSelected, options, view] + ); + + return ( + + + + + {view === 'raw' || view === 'all' ? ( + + ) : ( + + )} + + + ); +}; + +TopNComponent.displayName = 'TopNComponent'; + +export const TopN = React.memo(TopNComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/translations.ts b/x-pack/legacy/plugins/siem/public/components/top_n/translations.ts new file mode 100644 index 0000000000000..7db55fa94d42e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/top_n/translations.ts @@ -0,0 +1,23 @@ +/* + * 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 CLOSE = i18n.translate('xpack.siem.topN.closeButtonLabel', { + defaultMessage: 'Close', +}); + +export const ALL_EVENTS = i18n.translate('xpack.siem.topN.allEventsSelectLabel', { + defaultMessage: 'All events', +}); + +export const RAW_EVENTS = i18n.translate('xpack.siem.topN.rawEventsSelectLabel', { + defaultMessage: 'Raw events', +}); + +export const SIGNAL_EVENTS = i18n.translate('xpack.siem.topN.signalEventsSelectLabel', { + defaultMessage: 'Signal events', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx index 07ea165fcbb5c..86a9acc486b6d 100644 --- a/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback } from 'react'; -import styled from 'styled-components'; +import { EuiPopover } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; interface Props { /** @@ -26,34 +27,6 @@ interface Props { */ render: (showHoverContent: boolean) => JSX.Element; } - -const HoverActionsPanelContainer = styled.div` - color: ${({ theme }) => theme.eui.textColors.default}; - height: 100%; - position: relative; -`; - -HoverActionsPanelContainer.displayName = 'HoverActionsPanelContainer'; - -const HoverActionsPanel = React.memo<{ children: JSX.Element; show: boolean }>( - ({ children, show }) => ( - - {show ? children : null} - - ) -); - -HoverActionsPanel.displayName = 'HoverActionsPanel'; - -const WithHoverActionsContainer = styled.div` - display: flex; - flex-direction: row; - height: 100%; - padding-right: 5px; -`; - -WithHoverActionsContainer.displayName = 'WithHoverActionsContainer'; - /** * Decorates it's children with actions that are visible on hover. * This component does not enforce an opinion on the styling and @@ -68,20 +41,41 @@ export const WithHoverActions = React.memo( ({ alwaysShow = false, hoverContent, render }) => { const [showHoverContent, setShowHoverContent] = useState(false); const onMouseEnter = useCallback(() => { - setShowHoverContent(true); + // NOTE: the following read from the DOM is expensive, but not as + // expensive as the default behavior, which adds a div to the body, + // which-in turn performs a more expensive change to the layout + if (!document.body.classList.contains(IS_DRAGGING_CLASS_NAME)) { + setShowHoverContent(true); + } }, []); const onMouseLeave = useCallback(() => { setShowHoverContent(false); }, []); + const content = useMemo(() => <>{render(showHoverContent)}, [render, showHoverContent]); + + const isOpen = hoverContent != null && (showHoverContent || alwaysShow); + + const popover = useMemo(() => { + return ( + + {isOpen ? hoverContent : null} + + ); + }, [content, onMouseLeave, isOpen, alwaysShow, hoverContent]); + return ( - - <>{render(showHoverContent)} - - {hoverContent != null ? hoverContent : <>} - - +
+ {popover} +
); } ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts index e8019659d49c6..9eb4acbdb6164 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts @@ -501,10 +501,8 @@ describe('Detections Rules API', () => { test('check parameter url, query', async () => { await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find_statuses', { - query: { - ids: '["mySuperRuleId"]', - }, - method: 'GET', + body: '{"ids":["mySuperRuleId"]}', + method: 'POST', signal: abortCtrl.signal, }); }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 2dd6955581eff..5cc73e17662c6 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -266,8 +266,8 @@ export const getRuleStatusById = async ({ signal: AbortSignal; }): Promise => KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_STATUS_URL, { - method: 'GET', - query: { ids: JSON.stringify([id]) }, + method: 'POST', + body: JSON.stringify({ ids: [id] }), signal, }); @@ -289,8 +289,8 @@ export const getRulesStatusByIds = async ({ const res = await KibanaServices.get().http.fetch( DETECTION_ENGINE_RULES_STATUS_URL, { - method: 'GET', - query: { ids: JSON.stringify(ids) }, + method: 'POST', + body: JSON.stringify({ ids }), signal, } ); diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts index 0b369b4180fb8..83b3a8fdbb68c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts @@ -3,7 +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 { useEffect, useState, useRef } from 'react'; +import { isEmpty } from 'lodash/fp'; +import { useEffect, useMemo, useState, useRef } from 'react'; + import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { errorToToaster, useStateToaster } from '../../components/toasters'; @@ -19,11 +21,19 @@ export const useQuery = ({ errorMessage, filterQuery, histogramType, + indexToAdd, isInspected, stackByField, startDate, }: MatrixHistogramQueryProps) => { - const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const defaultIndex = useMemo(() => { + if (indexToAdd != null && !isEmpty(indexToAdd)) { + return [...configIndex, ...indexToAdd]; + } + return configIndex; + }, [configIndex, indexToAdd]); + const [, dispatchToaster] = useStateToaster(); const refetch = useRef(); const [loading, setLoading] = useState(false); @@ -96,6 +106,7 @@ export const useQuery = ({ errorMessage, filterQuery, histogramType, + indexToAdd, isInspected, stackByField, startDate, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx index 27ee552146092..5c89a7e25b7a4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx @@ -5,6 +5,7 @@ */ import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from './types'; +import { showAllOthersBucket } from '../../../../../common/constants'; import { SignalSearchResponse } from '../../../../containers/detection_engine/signals/types'; import * as i18n from './translations'; @@ -34,48 +35,56 @@ export const getSignalsHistogramQuery = ( additionalFilters: Array<{ bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; }> -) => ({ - aggs: { - signalsByGrouping: { - terms: { - field: stackByField, +) => { + const missing = showAllOthersBucket.includes(stackByField) + ? { missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, - order: { - _count: 'desc', + } + : {}; + + return { + aggs: { + signalsByGrouping: { + terms: { + field: stackByField, + ...missing, + order: { + _count: 'desc', + }, + size: 10, }, - size: 10, - }, - aggs: { - signals: { - date_histogram: { - field: '@timestamp', - fixed_interval: `${Math.floor((to - from) / 32)}ms`, - min_doc_count: 0, - extended_bounds: { - min: from, - max: to, + aggs: { + signals: { + date_histogram: { + field: '@timestamp', + fixed_interval: `${Math.floor((to - from) / 32)}ms`, + min_doc_count: 0, + extended_bounds: { + min: from, + max: to, + }, }, }, }, }, }, - }, - query: { - bool: { - filter: [ - ...additionalFilters, - { - range: { - '@timestamp': { - gte: from, - lte: to, + query: { + bool: { + filter: [ + ...additionalFilters, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, }, }, - }, - ], + ], + }, }, - }, -}); + }; +}; /** * Returns `true` when the signals histogram initial loading spinner should be shown diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx index e25442b31da4e..f2d722e5a66d7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx @@ -9,16 +9,20 @@ import numeral from '@elastic/numeral'; import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; +import uuid from 'uuid'; +import { LegendItem } from '../../../../components/charts/draggable_legend_item'; +import { escapeDataProviderId } from '../../../../components/drag_and_drop/helpers'; import { HeaderSection } from '../../../../components/header_section'; - import { Filter, esQuery, Query } from '../../../../../../../../../src/plugins/data/public'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; import { getDetectionEngineUrl } from '../../../../components/link_to'; +import { defaultLegendColors } from '../../../../components/matrix_histogram/utils'; import { InspectButtonContainer } from '../../../../components/inspect'; import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; import { MatrixLoader } from '../../../../components/matrix_histogram/matrix_loader'; +import { MatrixHistogramOption } from '../../../../components/matrix_histogram/types'; import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; import { navTabs } from '../../../home/home_navigations'; import { signalsHistogramOptions } from './config'; @@ -53,6 +57,9 @@ interface SignalsHistogramPanelProps { deleteQuery?: ({ id }: { id: string }) => void; filters?: Filter[]; from: number; + headerChildren?: React.ReactNode; + /** Override all defaults, and only display this field */ + onlyField?: string; query?: Query; legendPosition?: Position; panelHeight?: number; @@ -66,12 +73,21 @@ interface SignalsHistogramPanelProps { updateDateRange: (min: number, max: number) => void; } +const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ + text: fieldName, + value: fieldName, +}); + +const NO_LEGEND_DATA: LegendItem[] = []; + export const SignalsHistogramPanel = memo( ({ chartHeight, defaultStackByOption = signalsHistogramOptions[0], deleteQuery, filters, + headerChildren, + onlyField, query, from, legendPosition = 'right', @@ -85,11 +101,13 @@ export const SignalsHistogramPanel = memo( title = i18n.HISTOGRAM_HEADER, updateDateRange, }) => { + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); const [isInitialLoading, setIsInitialLoading] = useState(true); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); const [totalSignalsObj, setTotalSignalsObj] = useState(defaultTotalSignalsObj); const [selectedStackByOption, setSelectedStackByOption] = useState( - defaultStackByOption + onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) ); const { loading: isLoadingSignals, @@ -123,6 +141,21 @@ export const SignalsHistogramPanel = memo( const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]); + const legendItems: LegendItem[] = useMemo( + () => + signalsData?.aggregations?.signalsByGrouping?.buckets != null + ? signalsData.aggregations.signalsByGrouping.buckets.map((bucket, i) => ({ + color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, + dataProviderId: escapeDataProviderId( + `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` + ), + field: selectedStackByOption.value, + value: bucket.key, + })) + : NO_LEGEND_DATA, + [signalsData, selectedStackByOption.value] + ); + useEffect(() => { let canceled = false; @@ -138,7 +171,7 @@ export const SignalsHistogramPanel = memo( useEffect(() => { return () => { if (deleteQuery) { - deleteQuery({ id: DETECTIONS_HISTOGRAM_ID }); + deleteQuery({ id: uniqueQueryId }); } }; }, []); @@ -146,7 +179,7 @@ export const SignalsHistogramPanel = memo( useEffect(() => { if (refetch != null && setQuery != null) { setQuery({ - id: DETECTIONS_HISTOGRAM_ID, + id: uniqueQueryId, inspect: { dsl: [request], response: [response], @@ -197,46 +230,49 @@ export const SignalsHistogramPanel = memo( } }, [showLinkToSignals, urlSearch]); + const titleText = useMemo(() => (onlyField == null ? title : i18n.TOP(onlyField)), [ + onlyField, + title, + ]); + return ( - + + + + + {stackByOptions && ( + + )} + {headerChildren != null && headerChildren} + + {linkButton} + + + {isInitialLoading ? ( - <> - - - + ) : ( - <> - - - - {stackByOptions && ( - - )} - - {linkButton} - - - - - + )} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx index 5eb9beaaaf76a..6a116efb8f2f8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx @@ -14,7 +14,14 @@ jest.mock('../../../../lib/kibana'); describe('SignalsHistogram', () => { it('renders correctly', () => { const wrapper = shallow( - + ); expect(wrapper.find('Chart')).toBeTruthy(); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx index 40e5b8abde072..4bb7e9f6e122f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx @@ -12,11 +12,14 @@ import { Settings, ChartSizeArray, } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { EuiProgress } from '@elastic/eui'; import { useTheme } from '../../../../components/charts/common'; import { histogramDateTimeFormatter } from '../../../../components/utils'; +import { DraggableLegend } from '../../../../components/charts/draggable_legend'; +import { LegendItem } from '../../../../components/charts/draggable_legend_item'; + import { HistogramData } from './types'; const DEFAULT_CHART_HEIGHT = 174; @@ -24,18 +27,19 @@ const DEFAULT_CHART_HEIGHT = 174; interface HistogramSignalsProps { chartHeight?: number; from: number; + legendItems: LegendItem[]; legendPosition?: Position; loading: boolean; to: number; data: HistogramData[]; updateDateRange: (min: number, max: number) => void; } - export const SignalsHistogram = React.memo( ({ chartHeight = DEFAULT_CHART_HEIGHT, data, from, + legendItems, legendPosition = 'right', loading, to, @@ -62,29 +66,38 @@ export const SignalsHistogram = React.memo( /> )} - - + + + + - + - + - - + + +
+ + {legendItems.length > 0 && ( + + )} + + ); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts index 8c88fa4a5dae6..e7b76a48c7592 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts @@ -83,6 +83,12 @@ export const STACK_BY_USERS = i18n.translate( } ); +export const TOP = (fieldName: string) => + i18n.translate('xpack.siem.detectionEngine.signals.histogram.topNLabel', { + values: { fieldName }, + defaultMessage: `Top {fieldName}`, + }); + export const HISTOGRAM_HEADER = i18n.translate( 'xpack.siem.detectionEngine.signals.histogram.headerTitle', { diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx index d838b936a2d65..bd9743bdccb4b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx @@ -66,8 +66,8 @@ describe('Alerts by category', () => { ); }); - test('it does NOT render the subtitle', () => { - expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').exists()).toBe(false); + test('it renders the subtitle (to prevent layout thrashing)', () => { + expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').exists()).toBe(true); }); test('it renders the expected filter fields', () => { diff --git a/x-pack/plugins/maps/public/layers/sources/vector_feature_types.js b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx similarity index 71% rename from x-pack/plugins/maps/public/layers/sources/vector_feature_types.js rename to x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx index cc5f30389c4f3..dad1e0034b4e2 100644 --- a/x-pack/plugins/maps/public/layers/sources/vector_feature_types.js +++ b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx @@ -4,8 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const VECTOR_SHAPE_TYPES = { - POINT: 'POINT', - LINE: 'LINE', - POLYGON: 'POLYGON', -}; +export const EventsByDataset = () => 'mock EventsByDataset'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx index cc1f9b1cc5681..485fec31db240 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx @@ -4,18 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Position } from '@elastic/charts'; import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; import React, { useEffect, useMemo } from 'react'; +import uuid from 'uuid'; -import { Position } from '@elastic/charts'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import { SHOWING, UNIT } from '../../../components/events_viewer/translations'; -import { convertToBuildEsQuery } from '../../../lib/keury'; import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; -import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { + MatrixHisrogramConfigs, + MatrixHistogramOption, +} from '../../../components/matrix_histogram/types'; +import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; +import { navTabs } from '../../home/home_navigations'; import { eventsStackByOptions } from '../../hosts/navigation'; +import { convertToBuildEsQuery } from '../../../lib/keury'; import { useKibana, useUiSetting$ } from '../../../lib/kibana'; +import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; import { Filter, esQuery, @@ -24,12 +32,9 @@ import { } from '../../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../../store'; import { HostsTableType, HostsType } from '../../../store/hosts/model'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { InputsModelId } from '../../../store/inputs/constants'; import * as i18n from '../translations'; -import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; -import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../home/home_navigations'; const NO_FILTERS: Filter[] = []; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; @@ -38,36 +43,56 @@ const DEFAULT_STACK_BY = 'event.dataset'; const ID = 'eventsByDatasetOverview'; interface Props { + combinedQueries?: string; deleteQuery?: ({ id }: { id: string }) => void; filters?: Filter[]; from: number; + headerChildren?: React.ReactNode; indexPattern: IIndexPattern; + indexToAdd?: string[] | null; + onlyField?: string; query?: Query; + setAbsoluteRangeDatePickerTarget?: InputsModelId; setQuery: (params: { id: string; inspect: inputsModel.InspectQuery | null; loading: boolean; refetch: inputsModel.Refetch; }) => void; + showSpacer?: boolean; to: number; } +const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ + text: fieldName, + value: fieldName, +}); + const EventsByDatasetComponent: React.FC = ({ + combinedQueries, deleteQuery, filters = NO_FILTERS, from, + headerChildren, indexPattern, + indexToAdd, + onlyField, query = DEFAULT_QUERY, + setAbsoluteRangeDatePickerTarget, setQuery, + showSpacer = true, to, }) => { + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${ID}-${uuid.v4()}`, []); + useEffect(() => { return () => { if (deleteQuery) { - deleteQuery({ id: ID }); + deleteQuery({ id: uniqueQueryId }); } }; - }, [deleteQuery]); + }, [deleteQuery, uniqueQueryId]); const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); @@ -84,38 +109,62 @@ const EventsByDatasetComponent: React.FC = ({ const filterQuery = useMemo( () => - convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }), - [kibana, indexPattern, query, filters] + combinedQueries == null + ? convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }) + : combinedQueries, + [combinedQueries, kibana, indexPattern, query, filters] ); const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( () => ({ ...histogramConfigs, + stackByOptions: + onlyField != null ? [getHistogramOption(onlyField)] : histogramConfigs.stackByOptions, defaultStackByOption: - eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], + onlyField != null + ? getHistogramOption(onlyField) + : eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], legendPosition: Position.Right, subtitle: (totalCount: number) => `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + titleSize: onlyField == null ? 'm' : 's', }), - [] + [onlyField, defaultNumberFormat] ); + const headerContent = useMemo(() => { + if (onlyField == null || headerChildren != null) { + return ( + <> + {headerChildren} + {onlyField == null && eventsCountViewEventsButton} + + ); + } else { + return null; + } + }, [onlyField, headerChildren, eventsCountViewEventsButton]); + return ( ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx index 2db49e60193fc..82f4444728902 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx @@ -63,7 +63,7 @@ const OverviewComponent: React.FC = ({ from={from} indexPattern={indexPattern} query={query} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setQuery={setQuery} to={to} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx index 5f78c4c10eb37..feba80539a11b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx @@ -12,6 +12,7 @@ import { useSignalIndex } from '../../../containers/detection_engine/signals/use import { SetAbsoluteRangeDatePicker } from '../../network/types'; import { Filter, IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../../store'; +import { InputsModelId } from '../../../store/inputs/constants'; import * as i18n from '../translations'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; @@ -22,9 +23,13 @@ interface Props { deleteQuery?: ({ id }: { id: string }) => void; filters?: Filter[]; from: number; + headerChildren?: React.ReactNode; indexPattern: IIndexPattern; + /** Override all defaults, and only display this field */ + onlyField?: string; query?: Query; setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; + setAbsoluteRangeDatePickerTarget?: InputsModelId; setQuery: (params: { id: string; inspect: inputsModel.InspectQuery | null; @@ -38,15 +43,18 @@ const SignalsByCategoryComponent: React.FC = ({ deleteQuery, filters = NO_FILTERS, from, + headerChildren, + onlyField, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, + setAbsoluteRangeDatePickerTarget = 'global', setQuery, to, }) => { const { signalIndexName } = useSignalIndex(); const updateDateRangeCallback = useCallback( (min: number, max: number) => { - setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); + setAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, from: min, to: max }); }, [setAbsoluteRangeDatePicker] ); @@ -60,12 +68,14 @@ const SignalsByCategoryComponent: React.FC = ({ defaultStackByOption={defaultStackByOption} filters={filters} from={from} + headerChildren={headerChildren} + onlyField={onlyField} query={query} signalIndexName={signalIndexName} setQuery={setQuery} showTotalSignalsCount={true} - showLinkToSignals={true} - stackByOptions={signalsHistogramOptions} + showLinkToSignals={onlyField == null ? true : false} + stackByOptions={onlyField == null ? signalsHistogramOptions : undefined} legendPosition={'right'} to={to} title={i18n.SIGNAL_COUNT} diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts b/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts index 601a629d86e57..b7bee15e4c5bf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts @@ -38,6 +38,12 @@ export const SIGNAL_COUNT = i18n.translate('xpack.siem.overview.signalCountTitle defaultMessage: 'Signal count', }); +export const TOP = (fieldName: string) => + i18n.translate('xpack.siem.overview.topNLabel', { + values: { fieldName }, + defaultMessage: `Top {fieldName}`, + }); + export const VIEW_ALERTS = i18n.translate('xpack.siem.overview.viewAlertsButtonLabel', { defaultMessage: 'View alerts', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx index 62399891c9606..ae95a1316a600 100644 --- a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx @@ -10,6 +10,8 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; import ApolloClient from 'apollo-client'; +jest.mock('../../pages/overview/events_by_dataset'); + jest.mock('../../lib/kibana', () => { return { useKibana: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index e400360a5a5b2..94097df48949f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -254,9 +254,9 @@ export const getFindResultWithMultiHits = ({ export const ruleStatusRequest = () => requestMock.create({ - method: 'get', + method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, - query: { ids: ['someId'] }, + body: { ids: ['someId'] }, }); export const getImportRulesRequest = (hapiStream?: HapiReadableStream) => diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts index 89c9f34027120..d7c6d317227fa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts @@ -60,9 +60,9 @@ describe('find_statuses', () => { describe('request validation', () => { test('disallows singular id query param', async () => { const request = requestMock.create({ - method: 'get', + method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, - query: { id: ['someId'] }, + body: { id: ['someId'] }, }); const result = server.validate(request); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 6fee4d71a904e..8eed146537718 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -22,18 +22,18 @@ import { } from '../utils'; export const findRulesStatusesRoute = (router: IRouter) => { - router.get( + router.post( { path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, validate: { - query: buildRouteValidation(findRulesStatusesSchema), + body: buildRouteValidation(findRulesStatusesSchema), }, options: { tags: ['access:siem'], }, }, async (context, request, response) => { - const { query } = request; + const { body } = request; const siemResponse = buildSiemResponse(response); const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; @@ -50,7 +50,7 @@ export const findRulesStatusesRoute = (router: IRouter) => { } */ try { - const statuses = await query.ids.reduce>( + const statuses = await body.ids.reduce>( async (acc, id) => { const lastFiveErrorsForId = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index e56e8e5fe34d3..82d355d37e7bb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -556,7 +556,9 @@ describe('create rules schema', () => { test('language does not validate with something made up', () => { expect( - createRulesSchema.validate>({ + createRulesSchema.validate< + Partial & { language: string }> + >({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts index e01a8f40fcea4..9e23e4e3bad55 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -572,7 +572,9 @@ describe('patch rules schema', () => { test('language does not validate with something made up', () => { expect( - patchRulesSchema.validate>({ + patchRulesSchema.validate< + Partial & { language: string }> + >({ id: 'rule-1', description: 'some description', from: 'now-5m', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts index 85a38e296494a..743914ad070a2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts @@ -7,10 +7,16 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { list_and as listAnd } from '../response/schemas'; +import { + list_and as listAnd, + list_values as listValues, + list_values_operator as listOperator, +} from '../response/schemas'; export type ListsDefaultArrayC = t.Type; -type List = t.TypeOf; +export type List = t.TypeOf; +export type ListValues = t.TypeOf; +export type ListOperator = t.TypeOf; /** * Types the ListsDefaultArray as: diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index e8f9aad620ca0..be2f06590e81d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -575,7 +575,9 @@ describe('create rules schema', () => { test('language does not validate with something made up', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate< + Partial & { language: string }> + >({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh index 543c019067e8e..5ae4904e9e4ec 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh @@ -10,8 +10,12 @@ set -e ./check_env_variables.sh -# Example: ./find_rules_statuses_by_ids.sh '["12345","6789abc"]' +# Example: ./find_rules_statuses_by_ids.sh [\"12345\",\"6789abc\"] curl -g -k \ + -s \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find_statuses?ids=$1" \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find_statuses" \ + -d "{\"ids\": $1}" \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json new file mode 100644 index 0000000000000..33ae8df87e3c8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json @@ -0,0 +1,35 @@ +{ + "name": "List - and", + "description": "Query with a list that includes and. This rule should only produce signals when host.name exists and when both event.module is endgame and event.category is anything other than file", + "rule_id": "query-with-list-and", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "lists": [ + { + "field": "event.module", + "values_operator": "excluded", + "values_type": "match", + "values": [ + { + "name": "endgame" + } + ], + "and": [ + { + "field": "event.category", + "values_operator": "included", + "values_type": "match", + "values": [ + { + "name": "file" + } + ] + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json new file mode 100644 index 0000000000000..8ce9440fce219 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json @@ -0,0 +1,23 @@ +{ + "name": "List - excluded", + "description": "Query with a list of values_operator excluded. This rule should only produce signals when host.name exists and event.module is suricata", + "rule_id": "query-with-list-excluded", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "lists": [ + { + "field": "event.module", + "values_operator": "excluded", + "values_type": "match", + "values": [ + { + "name": "suricata" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json new file mode 100644 index 0000000000000..4ec014422a61b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json @@ -0,0 +1,18 @@ +{ + "name": "List - exists", + "description": "Query with a list that includes exists. This rule should only produce signals when host.name exists and event.action does not exist", + "rule_id": "query-with-list-exists", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "lists": [ + { + "field": "event.action", + "values_operator": "included", + "values_type": "exists" + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json new file mode 100644 index 0000000000000..ce4bf80b0a722 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json @@ -0,0 +1,54 @@ +{ + "name": "Query with a list", + "description": "Query with a list. This rule should only produce signals when either host.name exists and event.module is system and user.name is zeek or gdm OR when host.name exists and event.module is not endgame or zeek or system.", + "rule_id": "query-with-list", + "risk_score": 2, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "lists": [ + { + "field": "event.module", + "values_operator": "excluded", + "values_type": "match", + "values": [ + { + "name": "system" + } + ], + "and": [ + { + "field": "user.name", + "values_operator": "excluded", + "values_type": "match_all", + "values": [ + { + "name": "zeek" + }, + { + "name": "gdm" + } + ] + } + ] + }, + { + "field": "event.module", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "endgame" + }, + { + "name": "zeek" + }, + { + "name": "system" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json new file mode 100644 index 0000000000000..e1abac19b51e5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json @@ -0,0 +1,23 @@ +{ + "name": "List - match", + "description": "Query with a list that includes match. This rule should only produce signals when host.name exists and event.module is not suricata", + "rule_id": "query-with-list-match", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "lists": [ + { + "field": "event.module", + "values_operator": "included", + "values_type": "match", + "values": [ + { + "name": "suricata" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json new file mode 100644 index 0000000000000..1afbb1fee4785 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json @@ -0,0 +1,26 @@ +{ + "name": "List - match_all", + "description": "Query with a list that includes match_all. This rule should only produce signals when host.name exists and event.module is not suricata or auditd", + "rule_id": "query-with-list-match-all", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "lists": [ + { + "field": "event.module", + "values_operator": "included", + "values_type": "match_all", + "values": [ + { + "name": "suricata" + }, + { + "name": "auditd" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json new file mode 100644 index 0000000000000..08e38e9ffa53d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json @@ -0,0 +1,32 @@ +{ + "name": "List - or", + "description": "Query with a list that includes or. This rule should only produce signals when host.name exists and event.module is suricata OR when host.name exists and event.category is file", + "rule_id": "query-with-list-or", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "lists": [ + { + "field": "event.module", + "values_operator": "excluded", + "values_type": "match", + "values": [ + { + "name": "suricata" + } + ] + }, + { + "field": "event.category", + "values_operator": "excluded", + "values_type": "match", + "values": [ + { + "name": "file" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json deleted file mode 100644 index 997d03369a699..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "Query with a list", - "description": "Query with a list", - "rule_id": "query-with-list", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "user.name: root or user.name: admin", - "lists": [ - { - "field": "source.ip", - "values_operator": "included", - "values_type": "exists" - }, - { - "field": "host.name", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "rock01" - } - ], - "and": [ - { - "field": "host.id", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "123" - }, - { - "name": "678" - } - ] - } - ] - } - ] -} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 7a211c5631da6..3bdcc3f92f44c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSourceHit, SignalSearchResponse } from '../types'; +import { SignalSourceHit, SignalSearchResponse, BulkResponse, BulkItem } from '../types'; import { Logger, SavedObject, @@ -416,3 +416,68 @@ export const exampleFindRuleStatusResponse: ( }); export const mockLogger: Logger = loggingServiceMock.createLogger(); + +export const sampleBulkErrorItem = ( + { + status, + reason, + }: { + status: number; + reason: string; + } = { status: 400, reason: 'Invalid call' } +): BulkItem => { + return { + create: { + _index: 'mock_index', + _id: '123', + _version: 1, + status, + _shards: { + total: 1, + successful: 0, + failed: 1, + }, + error: { + type: 'Invalid', + reason, + shard: 'shard 123', + index: 'mock_index', + }, + }, + }; +}; + +export const sampleBulkItem = (): BulkItem => { + return { + create: { + _index: 'mock_index', + _id: '123', + _version: 1, + status: 200, + result: 'some result here', + _shards: { + total: 1, + successful: 1, + failed: 0, + }, + }, + }; +}; + +export const sampleEmptyBulkResponse = (): BulkResponse => ({ + took: 0, + errors: false, + items: [], +}); + +export const sampleBulkError = (): BulkResponse => ({ + took: 0, + errors: true, + items: [sampleBulkErrorItem()], +}); + +export const sampleBulkResponse = (): BulkResponse => ({ + took: 0, + errors: true, + items: [sampleBulkItem()], +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts new file mode 100644 index 0000000000000..ec8db77dac725 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts @@ -0,0 +1,1318 @@ +/* + * 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 { + buildQueryExceptions, + buildExceptions, + operatorBuilder, + buildExists, + buildMatch, + buildMatchAll, + evaluateValues, + formatQuery, + getLanguageBooleanOperator, +} from './build_exceptions_query'; +import { List } from '../routes/schemas/types/lists_default_array'; + +describe('build_exceptions_query', () => { + describe('getLanguageBooleanOperator', () => { + test('it returns value as uppercase if language is "lucene"', () => { + const result = getLanguageBooleanOperator({ language: 'lucene', value: 'not' }); + + expect(result).toEqual('NOT'); + }); + + test('it returns value as is if language is "kuery"', () => { + const result = getLanguageBooleanOperator({ language: 'kuery', value: 'not' }); + + expect(result).toEqual('not'); + }); + }); + + describe('operatorBuilder', () => { + describe('kuery', () => { + test('it returns "not " when operator is "excluded"', () => { + const operator = operatorBuilder({ operator: 'excluded', language: 'kuery' }); + + expect(operator).toEqual(' and '); + }); + + test('it returns empty string when operator is "included"', () => { + const operator = operatorBuilder({ operator: 'included', language: 'kuery' }); + + expect(operator).toEqual(' and not '); + }); + }); + + describe('lucene', () => { + test('it returns "NOT " when operator is "excluded"', () => { + const operator = operatorBuilder({ operator: 'excluded', language: 'lucene' }); + + expect(operator).toEqual(' AND '); + }); + + test('it returns empty string when operator is "included"', () => { + const operator = operatorBuilder({ operator: 'included', language: 'lucene' }); + + expect(operator).toEqual(' AND NOT '); + }); + }); + }); + + describe('buildExists', () => { + describe('kuery', () => { + test('it returns formatted wildcard string when operator is "excluded"', () => { + const query = buildExists({ operator: 'excluded', field: 'host.name', language: 'kuery' }); + + expect(query).toEqual(' and host.name:*'); + }); + + test('it returns formatted wildcard string when operator is "included"', () => { + const query = buildExists({ operator: 'included', field: 'host.name', language: 'kuery' }); + + expect(query).toEqual(' and not host.name:*'); + }); + }); + + describe('lucene', () => { + test('it returns formatted wildcard string when operator is "excluded"', () => { + const query = buildExists({ operator: 'excluded', field: 'host.name', language: 'lucene' }); + + expect(query).toEqual(' AND _exists_host.name'); + }); + + test('it returns formatted wildcard string when operator is "included"', () => { + const query = buildExists({ operator: 'included', field: 'host.name', language: 'lucene' }); + + expect(query).toEqual(' AND NOT _exists_host.name'); + }); + }); + }); + + describe('buildMatch', () => { + describe('kuery', () => { + test('it returns empty string if no items in "values"', () => { + const query = buildMatch({ + operator: 'included', + field: 'host.name', + values: [], + language: 'kuery', + }); + + expect(query).toEqual(''); + }); + + test('it returns formatted string when operator is "included"', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const query = buildMatch({ + operator: 'included', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(query).toEqual(' and not host.name:suricata'); + }); + + test('it returns formatted string when operator is "excluded"', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const query = buildMatch({ + operator: 'excluded', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(query).toEqual(' and host.name:suricata'); + }); + + // TODO: need to clean up types and maybe restrict values to one if type is 'match' + test('it returns formatted string when "values" includes more than one item', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const query = buildMatch({ + operator: 'included', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(query).toEqual(' and not host.name:suricata'); + }); + }); + + describe('lucene', () => { + test('it returns formatted string when operator is "included"', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const query = buildMatch({ + operator: 'included', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(query).toEqual(' AND NOT host.name:suricata'); + }); + + test('it returns formatted string when operator is "excluded"', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const query = buildMatch({ + operator: 'excluded', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(query).toEqual(' AND host.name:suricata'); + }); + + // TODO: need to clean up types and maybe restrict values to one if type is 'match' + test('it returns formatted string when "values" includes more than one item', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const query = buildMatch({ + operator: 'included', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(query).toEqual(' AND NOT host.name:suricata'); + }); + }); + }); + + describe('buildMatchAll', () => { + describe('kuery', () => { + test('it returns empty string if given an empty array for "values"', () => { + const exceptionSegment = buildMatchAll({ + operator: 'included', + field: 'host.name', + values: [], + language: 'kuery', + }); + + expect(exceptionSegment).toEqual(''); + }); + + test('it returns formatted string when "values" includes only one item', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'included', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(exceptionSegment).toEqual(' and not host.name:suricata'); + }); + + test('it returns formatted string when operator is "included"', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'included', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(exceptionSegment).toEqual(' and not host.name:(suricata or auditd)'); + }); + + test('it returns formatted string when operator is "excluded"', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'excluded', + field: 'host.name', + values, + language: 'kuery', + }); + + expect(exceptionSegment).toEqual(' and host.name:(suricata or auditd)'); + }); + }); + + describe('lucene', () => { + test('it returns formatted string when operator is "included"', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'included', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(exceptionSegment).toEqual(' AND NOT host.name:(suricata OR auditd)'); + }); + + test('it returns formatted string when operator is "excluded"', () => { + const values = [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'excluded', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(exceptionSegment).toEqual(' AND host.name:(suricata OR auditd)'); + }); + + test('it returns formatted string when "values" includes only one item', () => { + const values = [ + { + name: 'suricata', + }, + ]; + const exceptionSegment = buildMatchAll({ + operator: 'included', + field: 'host.name', + values, + language: 'lucene', + }); + + expect(exceptionSegment).toEqual(' AND NOT host.name:suricata'); + }); + }); + }); + + describe('evaluateValues', () => { + describe('kuery', () => { + test('it returns formatted wildcard string when "type" is "exists"', () => { + const list: List = { + values_operator: 'included', + values_type: 'exists', + field: 'host.name', + }; + const result = evaluateValues({ + list, + language: 'kuery', + }); + + expect(result).toEqual(' and not host.name:*'); + }); + + test('it returns formatted string when "type" is "match"', () => { + const list: List = { + values_operator: 'included', + values_type: 'match', + field: 'host.name', + values: [{ name: 'suricata' }], + }; + const result = evaluateValues({ + list, + language: 'kuery', + }); + + expect(result).toEqual(' and not host.name:suricata'); + }); + + test('it returns formatted string when "type" is "match_all"', () => { + const list: List = { + values_operator: 'included', + values_type: 'match_all', + field: 'host.name', + values: [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ], + }; + + const result = evaluateValues({ + list, + language: 'kuery', + }); + + expect(result).toEqual(' and not host.name:(suricata or auditd)'); + }); + }); + + describe('lucene', () => { + describe('kuery', () => { + test('it returns formatted wildcard string when "type" is "exists"', () => { + const list: List = { + values_operator: 'included', + values_type: 'exists', + field: 'host.name', + }; + const result = evaluateValues({ + list, + language: 'lucene', + }); + + expect(result).toEqual(' AND NOT _exists_host.name'); + }); + + test('it returns formatted string when "type" is "match"', () => { + const list: List = { + values_operator: 'included', + values_type: 'match', + field: 'host.name', + values: [{ name: 'suricata' }], + }; + const result = evaluateValues({ + list, + language: 'lucene', + }); + + expect(result).toEqual(' AND NOT host.name:suricata'); + }); + + test('it returns formatted string when "type" is "match_all"', () => { + const list: List = { + values_operator: 'included', + values_type: 'match_all', + field: 'host.name', + values: [ + { + name: 'suricata', + }, + { + name: 'auditd', + }, + ], + }; + + const result = evaluateValues({ + list, + language: 'lucene', + }); + + expect(result).toEqual(' AND NOT host.name:(suricata OR auditd)'); + }); + }); + }); + }); + + describe('formatQuery', () => { + test('it returns query if "exceptions" is empty array', () => { + const formattedQuery = formatQuery({ exceptions: [], query: 'a:*', language: 'kuery' }); + + expect(formattedQuery).toEqual('a:*'); + }); + + test('it returns expected query string when single exception in array', () => { + const formattedQuery = formatQuery({ + exceptions: [' and b:(value-1 or value-2) and not c:*'], + query: 'a:*', + language: 'kuery', + }); + + expect(formattedQuery).toEqual('(a:* and b:(value-1 or value-2) and not c:*)'); + }); + + test('it returns expected query string when multiple exceptions in array', () => { + const formattedQuery = formatQuery({ + exceptions: [' and b:(value-1 or value-2) and not c:*', ' and not d:*'], + query: 'a:*', + language: 'kuery', + }); + + expect(formattedQuery).toEqual( + '(a:* and b:(value-1 or value-2) and not c:*) or (a:* and not d:*)' + ); + }); + }); + + describe('buildExceptions', () => { + test('it returns empty array if empty lists array passed in', () => { + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists: [], + }); + + expect(query).toEqual([]); + }); + + test('it returns expected query when more than one item in list', () => { + // Equal to query && !(b && !c) -> (query AND NOT b) OR (query AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + }, + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value-3', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value-1 or value-2)', ' and c:value-3']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list item includes nested "and" value', () => { + // Equal to query && !(b || !c) -> (query AND NOT b AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value-3', + }, + ], + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value-1 or value-2) and c:value-3']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list item includes nested "and" value of empty array', () => { + // Equal to query && !(b || !c) -> (query AND NOT b AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + and: [], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value-1 or value-2)']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list item includes nested "and" value of null', () => { + // Equal to query && !(b || !c) -> (query AND NOT b AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + and: undefined, + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value-1 or value-2)']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes multiple items and nested "and" values', () => { + // Equal to query && !((b || !c) && d) -> (query AND NOT b AND c) OR (query AND NOT d) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value-3', + }, + ], + }, + ], + }, + { + field: 'd', + values_operator: 'included', + values_type: 'exists', + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value-1 or value-2) and c:value-3', ' and not d:*']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when language is "lucene"', () => { + // Equal to query && !((b || !c) && !d) -> (query AND NOT b AND c) OR (query AND d) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value-1', + }, + { + name: 'value-2', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value-3', + }, + ], + }, + ], + }, + { + field: 'e', + values_operator: 'excluded', + values_type: 'exists', + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'lucene', + lists, + }); + const expectedQuery = [' AND NOT b:(value-1 OR value-2) AND c:value-3', ' AND _exists_e']; + + expect(query).toEqual(expectedQuery); + }); + + describe('exists', () => { + test('it returns expected query when list includes single list item with values_operator of "included"', () => { + // Equal to query && !(b) -> (query AND NOT b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'exists', + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:*']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + // Equal to query && !(!b) -> (query AND b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'exists', + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:*']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes list item with "and" values', () => { + // Equal to query && !(!b || !c) -> (query AND b AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'exists', + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'exists', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:* and c:*']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes multiple items', () => { + // Equal to query && !((b || !c || d) && e) -> (query AND NOT b AND c AND NOT d) OR (query AND NOT e) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'exists', + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'exists', + }, + { + field: 'd', + values_operator: 'included', + values_type: 'exists', + }, + ], + }, + { + field: 'e', + values_operator: 'included', + values_type: 'exists', + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:* and c:* and not d:*', ' and not e:*']; + + expect(query).toEqual(expectedQuery); + }); + }); + + describe('match', () => { + test('it returns expected query when list includes single list item with values_operator of "included"', () => { + // Equal to query && !(b) -> (query AND NOT b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match', + values: [ + { + name: 'value', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:value']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + // Equal to query && !(!b) -> (query AND b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:value']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes list item with "and" values', () => { + // Equal to query && !(!b || !c) -> (query AND b AND c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'value', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'valueC', + }, + ], + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:value and c:valueC']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes multiple items', () => { + // Equal to query && !((b || !c || d) && e) -> (query AND NOT b AND c AND NOT d) OR (query AND NOT e) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match', + values: [ + { + name: 'value', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'valueC', + }, + ], + }, + { + field: 'd', + values_operator: 'included', + values_type: 'match', + values: [ + { + name: 'valueC', + }, + ], + }, + ], + }, + { + field: 'e', + values_operator: 'included', + values_type: 'match', + values: [ + { + name: 'valueC', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [ + ' and not b:value and c:valueC and not d:valueC', + ' and not e:valueC', + ]; + + expect(query).toEqual(expectedQuery); + }); + }); + + describe('match_all', () => { + test('it returns expected query when list includes single list item with values_operator of "included"', () => { + // Equal to query && !(b) -> (query AND NOT b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and not b:(value or value-1)']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + // Equal to query && !(!b) -> (query AND b) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:(value or value-1)']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes list item with "and" values', () => { + // Equal to query && !(!b || c) -> (query AND b AND NOT c) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'excluded', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + and: [ + { + field: 'c', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueC', + }, + { + name: 'value-2', + }, + ], + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [' and b:(value or value-1) and not c:(valueC or value-2)']; + + expect(query).toEqual(expectedQuery); + }); + + test('it returns expected query when list includes multiple items', () => { + // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match_all', + values: [ + { + name: 'valueC', + }, + { + name: 'value-2', + }, + ], + }, + { + field: 'd', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueD', + }, + { + name: 'value-3', + }, + ], + }, + ], + }, + { + field: 'e', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueE', + }, + { + name: 'value-4', + }, + ], + }, + ]; + const query = buildExceptions({ + query: 'a:*', + language: 'kuery', + lists, + }); + const expectedQuery = [ + ' and not b:(value or value-1) and c:(valueC or value-2) and not d:(valueD or value-3)', + ' and not e:(valueE or value-4)', + ]; + + expect(query).toEqual(expectedQuery); + }); + }); + }); + + describe('buildQueryExceptions', () => { + test('it returns original query if no lists exist', () => { + const query = buildQueryExceptions({ + query: 'host.name: *', + language: 'kuery', + lists: undefined, + }); + const expectedQuery = 'host.name: *'; + + expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + }); + + test('it returns original query if lists is empty array', () => { + const query = buildQueryExceptions({ query: 'host.name: *', language: 'kuery', lists: [] }); + const expectedQuery = 'host.name: *'; + + expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + }); + + test('it returns original query if lists is null', () => { + const query = buildQueryExceptions({ query: 'host.name: *', language: 'kuery', lists: null }); + const expectedQuery = 'host.name: *'; + + expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + }); + + test('it returns original query if lists is undefined', () => { + const query = buildQueryExceptions({ + query: 'host.name: *', + language: 'kuery', + lists: undefined, + }); + const expectedQuery = 'host.name: *'; + + expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + }); + + test('it returns expected query when lists exist and language is "kuery"', () => { + // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match_all', + values: [ + { + name: 'valueC', + }, + { + name: 'value-2', + }, + ], + }, + { + field: 'd', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueD', + }, + { + name: 'value-3', + }, + ], + }, + ], + }, + { + field: 'e', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueE', + }, + { + name: 'value-4', + }, + ], + }, + ]; + const query = buildQueryExceptions({ query: 'a:*', language: 'kuery', lists }); + const expectedQuery = + '(a:* and not b:(value or value-1) and c:(valueC or value-2) and not d:(valueD or value-3)) or (a:* and not e:(valueE or value-4))'; + + expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + }); + + test('it returns expected query when lists exist and language is "lucene"', () => { + // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) + // https://www.dcode.fr/boolean-expressions-calculator + const lists: List[] = [ + { + field: 'b', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'value', + }, + { + name: 'value-1', + }, + ], + and: [ + { + field: 'c', + values_operator: 'excluded', + values_type: 'match_all', + values: [ + { + name: 'valueC', + }, + { + name: 'value-2', + }, + ], + }, + { + field: 'd', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueD', + }, + { + name: 'value-3', + }, + ], + }, + ], + }, + { + field: 'e', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: 'valueE', + }, + { + name: 'value-4', + }, + ], + }, + ]; + const query = buildQueryExceptions({ query: 'a:*', language: 'lucene', lists }); + const expectedQuery = + '(a:* AND NOT b:(value OR value-1) AND c:(valueC OR value-2) AND NOT d:(valueD OR value-3)) OR (a:* AND NOT e:(valueE OR value-4))'; + + expect(query).toEqual([{ query: expectedQuery, language: 'lucene' }]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts new file mode 100644 index 0000000000000..21d1524796649 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts @@ -0,0 +1,203 @@ +/* + * 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 { Query } from '../../../../../../../../src/plugins/data/server'; +import { List, ListOperator, ListValues } from '../routes/schemas/types/lists_default_array'; +import { RuleAlertParams, Language } from '../types'; + +type Operators = 'and' | 'or' | 'not'; +type LuceneOperators = 'AND' | 'OR' | 'NOT'; + +export const getLanguageBooleanOperator = ({ + language, + value, +}: { + language: Language; + value: Operators; +}): Operators | LuceneOperators => { + switch (language) { + case 'lucene': + const luceneValues: Record = { and: 'AND', or: 'OR', not: 'NOT' }; + + return luceneValues[value]; + case 'kuery': + return value; + default: + return value; + } +}; + +export const operatorBuilder = ({ + operator, + language, +}: { + operator: ListOperator; + language: Language; +}): string => { + const and = getLanguageBooleanOperator({ + language, + value: 'and', + }); + const or = getLanguageBooleanOperator({ + language, + value: 'not', + }); + + switch (operator) { + case 'excluded': + return ` ${and} `; + case 'included': + return ` ${and} ${or} `; + default: + return ''; + } +}; + +export const buildExists = ({ + operator, + field, + language, +}: { + operator: ListOperator; + field: string; + language: Language; +}): string => { + const exceptionOperator = operatorBuilder({ operator, language }); + + switch (language) { + case 'kuery': + return `${exceptionOperator}${field}:*`; + case 'lucene': + return `${exceptionOperator}_exists_${field}`; + default: + return ''; + } +}; + +export const buildMatch = ({ + operator, + field, + values, + language, +}: { + operator: ListOperator; + field: string; + values: ListValues[]; + language: Language; +}): string => { + if (values.length > 0) { + const exceptionOperator = operatorBuilder({ operator, language }); + const [exception] = values; + + return `${exceptionOperator}${field}:${exception.name}`; + } else { + return ''; + } +}; + +export const buildMatchAll = ({ + operator, + field, + values, + language, +}: { + operator: ListOperator; + field: string; + values: ListValues[]; + language: Language; +}): string => { + switch (values.length) { + case 0: + return ''; + case 1: + return buildMatch({ operator, field, values, language }); + default: + const or = getLanguageBooleanOperator({ language, value: 'or' }); + const exceptionOperator = operatorBuilder({ operator, language }); + const matchAllValues = values.map(value => { + return value.name; + }); + + return `${exceptionOperator}${field}:(${matchAllValues.join(` ${or} `)})`; + } +}; + +export const evaluateValues = ({ list, language }: { list: List; language: Language }): string => { + const { values_operator: operator, values_type: type, field, values } = list; + switch (type) { + case 'exists': + return buildExists({ operator, field, language }); + case 'match': + return buildMatch({ operator, field, values: values ?? [], language }); + case 'match_all': + return buildMatchAll({ operator, field, values: values ?? [], language }); + default: + return ''; + } +}; + +export const formatQuery = ({ + exceptions, + query, + language, +}: { + exceptions: string[]; + query: string; + language: Language; +}): string => { + if (exceptions.length > 0) { + const or = getLanguageBooleanOperator({ language, value: 'or' }); + const formattedExceptions = exceptions.map(exception => { + return `(${query}${exception})`; + }); + + return formattedExceptions.join(` ${or} `); + } else { + return query; + } +}; + +export const buildExceptions = ({ + query, + lists, + language, +}: { + query: string; + lists: List[]; + language: Language; +}): string[] => { + return lists.reduce((accum, listItem) => { + const { and, ...exceptionDetails } = { ...listItem }; + const andExceptionsSegments = and ? buildExceptions({ query, lists: and, language }) : []; + const exceptionSegment = evaluateValues({ list: exceptionDetails, language }); + const exception = [...exceptionSegment, ...andExceptionsSegments]; + + return [...accum, exception.join('')]; + }, []); +}; + +export const buildQueryExceptions = ({ + query, + language, + lists, +}: { + query: string; + language: Language; + lists: RuleAlertParams['lists']; +}): Query[] => { + if (lists && lists !== null) { + const exceptions = buildExceptions({ lists, language, query }); + const formattedQuery = formatQuery({ exceptions, language, query }); + + return [ + { + query: formattedQuery, + language, + }, + ]; + } else { + return [{ query, language }]; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts index 510667b211d25..c34a0010574af 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts @@ -35,7 +35,7 @@ describe('get_filter', () => { describe('getQueryFilter', () => { test('it should work with an empty filter as kuery', () => { - const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*']); + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); expect(esQuery).toEqual({ bool: { must: [], @@ -60,7 +60,7 @@ describe('get_filter', () => { }); test('it should work with an empty filter as lucene', () => { - const esQuery = getQueryFilter('host.name: linux', 'lucene', [], ['auditbeat-*']); + const esQuery = getQueryFilter('host.name: linux', 'lucene', [], ['auditbeat-*'], []); expect(esQuery).toEqual({ bool: { must: [ @@ -102,7 +102,8 @@ describe('get_filter', () => { }, }, ], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -145,7 +146,8 @@ describe('get_filter', () => { }, }, ], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -194,7 +196,8 @@ describe('get_filter', () => { 'host.name: windows', 'kuery', [query, exists], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -252,7 +255,8 @@ describe('get_filter', () => { }, }, ], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -300,7 +304,8 @@ describe('get_filter', () => { }, }, ], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -349,7 +354,8 @@ describe('get_filter', () => { }, }, ], - ['auditbeat-*'] + ['auditbeat-*'], + [] ); expect(esQuery).toEqual({ bool: { @@ -368,6 +374,108 @@ describe('get_filter', () => { }, }); }); + + test('it should work with a list', () => { + const esQuery = getQueryFilter( + 'host.name: linux', + 'kuery', + [], + ['auditbeat-*'], + [ + { + field: 'event.module', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'suricata', + }, + ], + }, + ] + ); + expect(esQuery).toEqual({ + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'host.name': 'linux', + }, + }, + ], + }, + }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'event.module': 'suricata', + }, + }, + ], + }, + }, + ], + }, + }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('it should work with an empty list', () => { + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); + expect(esQuery).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('it should work when lists has value null', () => { + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], null); + expect(esQuery).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('it should work when lists has value undefined', () => { + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], undefined); + expect(esQuery).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); }); describe('getFilter', () => { @@ -380,6 +488,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], + lists: undefined, }); expect(filter).toEqual({ bool: { @@ -414,6 +523,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], + lists: undefined, }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -428,6 +538,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], + lists: undefined, }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -442,6 +553,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: undefined, + lists: undefined, }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -455,6 +567,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: ['auditbeat-*'], + lists: undefined, }); expect(filter).toEqual({ bool: { @@ -478,6 +591,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], + lists: undefined, }) ).rejects.toThrow('savedId parameter should be defined'); }); @@ -492,6 +606,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: undefined, + lists: undefined, }) ).rejects.toThrow('savedId parameter should be defined'); }); @@ -506,6 +621,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: undefined, + lists: undefined, }) ).rejects.toThrow('Unsupported Rule of type "machine_learning" supplied to getFilter'); }); @@ -515,7 +631,8 @@ describe('get_filter', () => { '(event.module:suricata and event.kind:alert) and suricata.eve.alert.signature_id: (2610182 or 2610183 or 2610184 or 2610185 or 2610186 or 2610187)', 'kuery', [], - ['my custom index'] + ['my custom index'], + [] ); expect(esQuery).toEqual({ bool: { @@ -644,5 +761,68 @@ describe('get_filter', () => { }, }); }); + + test('returns a query when given a list', async () => { + const filter = await getFilter({ + type: 'query', + filters: undefined, + language: 'kuery', + query: 'host.name: siem', + savedId: undefined, + services: servicesMock, + index: ['auditbeat-*'], + lists: [ + { + field: 'event.module', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'suricata', + }, + ], + }, + ], + }); + expect(filter).toEqual({ + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'host.name': 'siem', + }, + }, + ], + }, + }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'event.module': 'suricata', + }, + }, + ], + }, + }, + ], + }, + }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts index 82a50222dc351..e900f78609e1e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts @@ -13,21 +13,24 @@ import { esFilters, IIndexPattern, } from '../../../../../../../../src/plugins/data/server'; -import { PartialFilter, RuleAlertParams } from '../types'; +import { PartialFilter, RuleAlertParams, Language } from '../types'; import { BadRequestError } from '../errors/bad_request_error'; +import { buildQueryExceptions } from './build_exceptions_query'; export const getQueryFilter = ( query: string, - language: string, + language: Language, filters: PartialFilter[], - index: string[] + index: string[], + lists: RuleAlertParams['lists'] ) => { const indexPattern = { fields: [], title: index.join(), } as IIndexPattern; - const queries: Query[] = [{ query, language }]; + const queries: Query[] = buildQueryExceptions({ query, language, lists }); + const config = { allowLeadingWildcards: true, queryStringOptions: { analyze_wildcard: true }, @@ -45,18 +48,19 @@ export const getQueryFilter = ( interface GetFilterArgs { type: RuleAlertParams['type']; filters: PartialFilter[] | undefined | null; - language: string | undefined | null; + language: Language | undefined | null; query: string | undefined | null; savedId: string | undefined | null; services: AlertServices; index: string[] | undefined | null; + lists: RuleAlertParams['lists']; } interface QueryAttributes { // NOTE: doesn't match Query interface query: { query: string; - language: string; + language: Language; }; filters: PartialFilter[]; } @@ -69,11 +73,12 @@ export const getFilter = async ({ services, type, query, + lists, }: GetFilterArgs): Promise => { switch (type) { case 'query': { if (query != null && language != null && index != null) { - return getQueryFilter(query, language, filters || [], index); + return getQueryFilter(query, language, filters || [], index, lists); } else { throw new BadRequestError('query, filters, and index parameter should be defined'); } @@ -90,13 +95,14 @@ export const getFilter = async ({ savedObject.attributes.query.query, savedObject.attributes.query.language, savedObject.attributes.filters, - index + index, + lists ); } catch (err) { // saved object does not exist, so try and fall back if the user pushed // any additional language, query, filters, etc... if (query != null && language != null && index != null) { - return getQueryFilter(query, language, filters || [], index); + return getQueryFilter(query, language, filters || [], index, lists); } else { // user did not give any additional fall back mechanism for generating a rule // rethrow error for activity monitoring diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 0357f906f8035..efb1338fd480e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -66,6 +66,7 @@ export const signalRulesAlertType = ({ query, to, type, + lists, } = params; const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); let hasError: boolean = false; @@ -200,6 +201,7 @@ export const signalRulesAlertType = ({ savedId, services, index: inputIndex, + lists, }); const noReIndex = buildEventsSearchQuery({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts index fc33d0e15e43f..4373a35cac0c5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts @@ -10,7 +10,7 @@ import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { SignalSearchResponse, BulkResponse } from './types'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; -import { generateId, makeFloatString } from './utils'; +import { generateId, makeFloatString, errorAggregator } from './utils'; import { buildBulkBody } from './build_bulk_body'; import { Logger } from '../../../../../../../../src/core/server'; @@ -134,17 +134,10 @@ export const singleBulkCreate = async ({ logger.debug(`took property says bulk took: ${response.took} milliseconds`); if (response.errors) { - const itemsWithErrors = response.items.filter(item => item.create.error); - const errorCountsByStatus = countBy(itemsWithErrors, item => item.create.status); - delete errorCountsByStatus['409']; // Duplicate signals are expected - - if (!isEmpty(errorCountsByStatus)) { + const errorCountByMessage = errorAggregator(response, [409]); + if (!isEmpty(errorCountByMessage)) { logger.error( - `[-] bulkResponse had errors with response statuses:counts of...\n${JSON.stringify( - errorCountsByStatus, - null, - 2 - )}` + `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` ); } } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index 040e32aa0d360..a7556d992d20a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -59,35 +59,35 @@ export interface SignalSource { }; } +export interface BulkItem { + create: { + _index: string; + _type?: string; + _id: string; + _version: number; + result?: string; + _shards?: { + total: number; + successful: number; + failed: number; + }; + _seq_no?: number; + _primary_term?: number; + status: number; + error?: { + type: string; + reason: string; + index_uuid?: string; + shard: string; + index: string; + }; + }; +} + export interface BulkResponse { took: number; errors: boolean; - items: [ - { - create: { - _index: string; - _type?: string; - _id: string; - _version: number; - result?: string; - _shards?: { - total: number; - successful: number; - failed: number; - }; - _seq_no?: number; - _primary_term?: number; - status: number; - error?: { - type: string; - reason: string; - index_uuid?: string; - shard: string; - index: string; - }; - }; - } - ]; + items: BulkItem[]; } export interface MGetResponse { @@ -169,3 +169,5 @@ export interface RuleAlertAttributes extends AlertAttributes { ruleId: string; }; } + +export type BulkResponseErrorAggregation = Record; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts index 873e06fcbb44e..e3a1b0c052aca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts @@ -13,8 +13,18 @@ import { parseScheduleDates, getDriftTolerance, getGapBetweenRuns, + errorAggregator, } from './utils'; +import { BulkResponseErrorAggregation } from './types'; + +import { + sampleBulkResponse, + sampleEmptyBulkResponse, + sampleBulkError, + sampleBulkErrorItem, +} from './__mocks__/es_results'; + describe('utils', () => { const anchor = '2020-01-01T06:06:06.666Z'; const unix = moment(anchor).valueOf(); @@ -351,4 +361,206 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); }); + + describe('errorAggregator', () => { + test('it should aggregate with an empty object when given an empty bulk response', () => { + const empty = sampleEmptyBulkResponse(); + const aggregated = errorAggregator(empty, []); + const expected: BulkResponseErrorAggregation = {}; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate with an empty object when given a valid bulk response with no errors', () => { + const validResponse = sampleBulkResponse(); + const aggregated = errorAggregator(validResponse, []); + const expected: BulkResponseErrorAggregation = {}; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate with a single error when given a single error item', () => { + const singleError = sampleBulkError(); + const aggregated = errorAggregator(singleError, []); + const expected: BulkResponseErrorAggregation = { + 'Invalid call': { + count: 1, + statusCode: 400, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate two errors with a correct count when given the same two error items', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem(); + const item2 = sampleBulkErrorItem(); + twoAggregatedErrors.items = [item1, item2]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Invalid call': { + count: 2, + statusCode: 400, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate three errors with a correct count when given the same two error items', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem(); + const item2 = sampleBulkErrorItem(); + const item3 = sampleBulkErrorItem(); + twoAggregatedErrors.items = [item1, item2, item3]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Invalid call': { + count: 3, + statusCode: 400, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate two distinct errors with the correct count of 1 for each error type', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item2 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + twoAggregatedErrors.items = [item1, item2]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Parse Error': { + count: 1, + statusCode: 400, + }, + 'Bad Network': { + count: 1, + statusCode: 500, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate two of the same errors with the correct count of 2 for each error type', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item2 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item3 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item4 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + twoAggregatedErrors.items = [item1, item2, item3, item4]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Parse Error': { + count: 2, + statusCode: 400, + }, + 'Bad Network': { + count: 2, + statusCode: 500, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate three of the same errors with the correct count of 2 for each error type', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item2 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item3 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item4 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item5 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item6 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + twoAggregatedErrors.items = [item1, item2, item3, item4, item5, item6]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Parse Error': { + count: 2, + statusCode: 400, + }, + 'Bad Network': { + count: 2, + statusCode: 500, + }, + 'Bad Gateway': { + count: 2, + statusCode: 502, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it should aggregate a mix of errors with the correct aggregate count of each', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 400, reason: 'Parse Error' }); + const item2 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item3 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item4 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item5 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item6 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + twoAggregatedErrors.items = [item1, item2, item3, item4, item5, item6]; + const aggregated = errorAggregator(twoAggregatedErrors, []); + const expected: BulkResponseErrorAggregation = { + 'Parse Error': { + count: 1, + statusCode: 400, + }, + 'Bad Network': { + count: 2, + statusCode: 500, + }, + 'Bad Gateway': { + count: 3, + statusCode: 502, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it will ignore error single codes such as 409', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 409, reason: 'Conflict Error' }); + const item2 = sampleBulkErrorItem({ status: 409, reason: 'Conflict Error' }); + const item3 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item4 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item5 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item6 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + twoAggregatedErrors.items = [item1, item2, item3, item4, item5, item6]; + const aggregated = errorAggregator(twoAggregatedErrors, [409]); + const expected: BulkResponseErrorAggregation = { + 'Bad Network': { + count: 1, + statusCode: 500, + }, + 'Bad Gateway': { + count: 3, + statusCode: 502, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it will ignore two error codes such as 409 and 502', () => { + const twoAggregatedErrors = sampleBulkError(); + const item1 = sampleBulkErrorItem({ status: 409, reason: 'Conflict Error' }); + const item2 = sampleBulkErrorItem({ status: 409, reason: 'Conflict Error' }); + const item3 = sampleBulkErrorItem({ status: 500, reason: 'Bad Network' }); + const item4 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item5 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + const item6 = sampleBulkErrorItem({ status: 502, reason: 'Bad Gateway' }); + twoAggregatedErrors.items = [item1, item2, item3, item4, item5, item6]; + const aggregated = errorAggregator(twoAggregatedErrors, [409, 502]); + const expected: BulkResponseErrorAggregation = { + 'Bad Network': { + count: 1, + statusCode: 500, + }, + }; + expect(aggregated).toEqual(expected); + }); + + test('it will return an empty object given valid inputs and status codes to ignore', () => { + const bulkResponse = sampleBulkResponse(); + const aggregated = errorAggregator(bulkResponse, [409, 502]); + const expected: BulkResponseErrorAggregation = {}; + expect(aggregated).toEqual(expected); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts index 49af310db559f..077d3a9279c5e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts @@ -7,6 +7,7 @@ import { createHash } from 'crypto'; import moment from 'moment'; import dateMath from '@elastic/datemath'; import { parseDuration } from '../../../../../../../plugins/alerting/server'; +import { BulkResponse, BulkResponseErrorAggregation } from './types'; export const generateId = ( docIndex: string, @@ -91,3 +92,45 @@ export const getGapBetweenRuns = ({ }; export const makeFloatString = (num: number): string => Number(num).toFixed(2); + +/** + * Given a BulkResponse this will return an aggregation based on the errors if any exist + * from the BulkResponse. Errors are aggregated on the reason as the unique key. + * + * Example would be: + * { + * 'Parse Error': { + * count: 100, + * statusCode: 400, + * }, + * 'Internal server error': { + * count: 3, + * statusCode: 500, + * } + * } + * If this does not return any errors then you will get an empty object like so: {} + * @param response The bulk response to aggregate based on the error message + * @param ignoreStatusCodes Optional array of status codes to ignore when creating aggregate error messages + * @returns The aggregated example as shown above. + */ +export const errorAggregator = ( + response: BulkResponse, + ignoreStatusCodes: number[] +): BulkResponseErrorAggregation => { + return response.items.reduce((accum, item) => { + if (item.create.error != null && !ignoreStatusCodes.includes(item.create.status)) { + if (accum[item.create.error.reason] == null) { + accum[item.create.error.reason] = { + count: 1, + statusCode: item.create.status, + }; + } else { + accum[item.create.error.reason] = { + count: accum[item.create.error.reason].count + 1, + statusCode: item.create.status, + }; + } + } + return accum; + }, Object.create(null)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index 035f1b10ff8b2..d87c226bf957a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -24,7 +24,7 @@ export interface ThreatParams { technique: IMitreAttack[]; } -// Notice below we are using lists: ListsDefaultArraySchema[]; which is coming directly from the response output section. +// Notice below we are using lists: ListsAndArraySchema[]; which is coming directly from the response output section. // TODO: Eventually this whole RuleAlertParams will be replaced with io-ts. For now we can slowly strangle it out and reduce duplicate types // We don't have the input types defined through io-ts just yet but as we being introducing types from there we will more and more remove // types and share them between input and output schema but have an input Rule Schema and an output Rule Schema. @@ -34,6 +34,8 @@ export interface Meta { kibana_siem_app_url?: string | undefined; } +export type Language = 'kuery' | 'lucene'; + export interface RuleAlertParams { actions: RuleAlertAction[]; anomalyThreshold: number | undefined; @@ -47,7 +49,7 @@ export interface RuleAlertParams { index: string[] | undefined | null; interval: string; ruleId: string | undefined | null; - language: string | undefined | null; + language: Language | undefined | null; maxSignals: number; machineLearningJobId: string | undefined; riskScore: number; diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts index 3a4281b980cc4..63649a1064b02 100644 --- a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts @@ -3,9 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { showAllOthersBucket } from '../../../common/constants'; import { createQueryFilterClauses, calculateTimeSeriesInterval } from '../../utils/build_query'; import { MatrixHistogramRequestOptions } from '../framework'; +import * as i18n from './translations'; + export const buildEventsOverTimeQuery = ({ filterQuery, timerange: { from, to }, @@ -41,11 +45,19 @@ export const buildEventsOverTimeQuery = ({ }, }, }; + + const missing = + stackByField != null && showAllOthersBucket.includes(stackByField) + ? { + missing: stackByField?.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, + } + : {}; + return { eventActionGroup: { terms: { field: stackByField, - missing: 'All others', + ...missing, order: { _count: 'desc', }, diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/translations.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/translations.ts new file mode 100644 index 0000000000000..413acaa2d4b0a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALL_OTHERS = i18n.translate( + 'xpack.siem.detectionEngine.signals.histogram.allOthersGroupingLabel', + { + defaultMessage: 'All others', + } +); diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts index 8dedd4672eeae..985b51891da99 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts @@ -6,10 +6,20 @@ import * as t from 'io-ts'; -export const DynamicSettingsType = t.type({ - heartbeatIndices: t.string, +export const CertificatesStatesThresholdType = t.interface({ + warningState: t.number, + errorState: t.number, }); +export const DynamicSettingsType = t.intersection([ + t.type({ + heartbeatIndices: t.string, + }), + t.partial({ + certificatesThresholds: CertificatesStatesThresholdType, + }), +]); + export const DynamicSettingsSaveType = t.intersection([ t.type({ success: t.boolean, @@ -21,7 +31,12 @@ export const DynamicSettingsSaveType = t.intersection([ export type DynamicSettings = t.TypeOf; export type DynamicSettingsSaveResponse = t.TypeOf; +export type CertificatesStatesThreshold = t.TypeOf; export const defaultDynamicSettings: DynamicSettings = { heartbeatIndices: 'heartbeat-8*', + certificatesThresholds: { + errorState: 7, + warningState: 30, + }, }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx index d149e7a6deb5a..ec2081d715554 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx @@ -9,8 +9,7 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; -import { Axis, Chart, Position, timeFormatter, Settings } from '@elastic/charts'; -import { SeriesIdentifier } from '@elastic/charts/dist/chart_types/xy_chart/utils/series'; +import { Axis, Chart, Position, timeFormatter, Settings, SeriesIdentifier } from '@elastic/charts'; import { getChartDateLabel } from '../../../lib/helper'; import { LocationDurationLine } from '../../../../common/types'; import { DurationLineSeriesList } from './duration_line_series_list'; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap new file mode 100644 index 0000000000000..36bc9bb860211 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificateForm shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap new file mode 100644 index 0000000000000..93151198c0f49 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificateForm shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx new file mode 100644 index 0000000000000..a3158f3d72445 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx @@ -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 React from 'react'; +import { CertificateExpirationForm } from '../certificate_form'; +import { shallowWithRouter } from '../../../lib'; + +describe('CertificateForm', () => { + it('shallow renders expected elements for valid props', () => { + expect( + shallowWithRouter( + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx new file mode 100644 index 0000000000000..654d51019d4e5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx @@ -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 React from 'react'; +import { IndicesForm } from '../indices_form'; +import { shallowWithRouter } from '../../../lib'; + +describe('CertificateForm', () => { + it('shallow renders expected elements for valid props', () => { + expect( + shallowWithRouter( + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx new file mode 100644 index 0000000000000..5103caee1e1c0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useSelector } from 'react-redux'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiCode, + EuiFieldNumber, + EuiTitle, + EuiSpacer, + EuiSelect, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { defaultDynamicSettings, DynamicSettings } from '../../../common/runtime_types'; +import { selectDynamicSettings } from '../../state/selectors'; + +type NumStr = string | number; + +export type OnFieldChangeType = (field: string, value?: NumStr) => void; + +export interface SettingsFormProps { + onChange: OnFieldChangeType; + formFields: DynamicSettings | null; + fieldErrors: any; + isDisabled: boolean; +} + +export const CertificateExpirationForm: React.FC = ({ + onChange, + formFields, + fieldErrors, + isDisabled, +}) => { + const dss = useSelector(selectDynamicSettings); + + return ( + <> + +

+ +

+
+ + + +

+ } + description={ + + } + > + {defaultDynamicSettings?.certificatesThresholds?.errorState} + ), + }} + /> + } + isInvalid={!!fieldErrors?.certificatesThresholds?.errorState} + label={ + + } + > + + + + onChange( + 'certificatesThresholds.errorState', + value === '' ? undefined : Number(value) + ) + } + /> + + + + + + + {defaultDynamicSettings?.certificatesThresholds?.warningState} + ), + }} + /> + } + isInvalid={!!fieldErrors?.certificatesThresholds?.warningState} + label={ + + } + > + + + + onChange('certificatesThresholds.warningState', Number(event.currentTarget.value)) + } + /> + + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx new file mode 100644 index 0000000000000..c28eca2ea229e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useSelector } from 'react-redux'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiCode, + EuiFieldText, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; +import { defaultDynamicSettings } from '../../../common/runtime_types'; +import { selectDynamicSettings } from '../../state/selectors'; +import { SettingsFormProps } from './certificate_form'; + +export const IndicesForm: React.FC = ({ + onChange, + formFields, + fieldErrors, + isDisabled, +}) => { + const dss = useSelector(selectDynamicSettings); + + return ( + <> + +

+ +

+
+ + + + + } + description={ + + } + > + {defaultDynamicSettings.heartbeatIndices}, + }} + /> + } + isInvalid={!!fieldErrors?.heartbeatIndices} + label={ + + } + > + onChange('heartbeatIndices', event.currentTarget.value)} + /> + + + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx index 765b0e3c664bc..049dffecd3f2e 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx @@ -9,46 +9,54 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, - EuiCode, - EuiDescribedFormGroup, - EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiForm, - EuiFormRow, EuiPanel, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { connect } from 'react-redux'; -import { isEqual } from 'lodash'; +import { useDispatch, useSelector } from 'react-redux'; +import { cloneDeep, isEqual, set } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Link } from 'react-router-dom'; -import { AppState } from '../state'; import { selectDynamicSettings } from '../state/selectors'; -import { DynamicSettingsState } from '../state/reducers/dynamic_settings'; import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic_settings'; -import { defaultDynamicSettings, DynamicSettings } from '../../common/runtime_types'; +import { DynamicSettings } from '../../common/runtime_types'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { OVERVIEW_ROUTE } from '../../common/constants'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { UptimePage, useUptimeTelemetry } from '../hooks'; +import { IndicesForm } from '../components/settings/indices_form'; +import { + CertificateExpirationForm, + OnFieldChangeType, +} from '../components/settings/certificate_form'; + +const getFieldErrors = (formFields: DynamicSettings | null) => { + if (formFields) { + const blankStr = 'May not be blank'; + const { certificatesThresholds, heartbeatIndices } = formFields; + const heartbeatIndErr = heartbeatIndices.match(/^\S+$/) ? '' : blankStr; + const errorStateErr = certificatesThresholds?.errorState ? null : blankStr; + const warningStateErr = certificatesThresholds?.warningState ? null : blankStr; + return { + heartbeatIndices: heartbeatIndErr, + certificatesThresholds: + errorStateErr || warningStateErr + ? { + errorState: errorStateErr, + warningState: warningStateErr, + } + : null, + }; + } + return null; +}; -interface Props { - dynamicSettingsState: DynamicSettingsState; -} - -interface DispatchProps { - dispatchGetDynamicSettings: typeof getDynamicSettings; - dispatchSetDynamicSettings: typeof setDynamicSettings; -} +export const SettingsPage = () => { + const dss = useSelector(selectDynamicSettings); -export const SettingsPageComponent = ({ - dynamicSettingsState: dss, - dispatchGetDynamicSettings, - dispatchSetDynamicSettings, -}: Props & DispatchProps) => { const settingsBreadcrumbText = i18n.translate('xpack.uptime.settingsBreadcrumbText', { defaultMessage: 'Settings', }); @@ -56,9 +64,11 @@ export const SettingsPageComponent = ({ useUptimeTelemetry(UptimePage.Settings); + const dispatch = useDispatch(); + useEffect(() => { - dispatchGetDynamicSettings({}); - }, [dispatchGetDynamicSettings]); + dispatch(getDynamicSettings({})); + }, [dispatch]); const [formFields, setFormFields] = useState(dss.settings || null); @@ -66,22 +76,22 @@ export const SettingsPageComponent = ({ setFormFields({ ...dss.settings }); } - const fieldErrors = formFields && { - heartbeatIndices: formFields.heartbeatIndices.match(/^\S+$/) ? null : 'May not be blank', - }; + const fieldErrors = getFieldErrors(formFields); + const isFormValid = !(fieldErrors && Object.values(fieldErrors).find(v => !!v)); - const onChangeFormField = (field: keyof DynamicSettings, value: any) => { + const onChangeFormField: OnFieldChangeType = (field, value) => { if (formFields) { - formFields[field] = value; - setFormFields({ ...formFields }); + const newFormFields = cloneDeep(formFields); + set(newFormFields, field, value); + setFormFields(cloneDeep(newFormFields)); } }; const onApply = (event: React.FormEvent) => { event.preventDefault(); if (formFields) { - dispatchSetDynamicSettings(formFields); + dispatch(setDynamicSettings(formFields)); } }; @@ -128,68 +138,18 @@ export const SettingsPageComponent = ({
- -

- -

-
- - - - - } - description={ - - } - > - {defaultDynamicSettings.heartbeatIndices} - ), - }} - /> - } - isInvalid={!!fieldErrors?.heartbeatIndices} - label={ - - } - > - - onChangeFormField('heartbeatIndices', event.currentTarget.value) - } - /> - - + + @@ -230,18 +190,3 @@ export const SettingsPageComponent = ({ ); }; - -const mapStateToProps = (state: AppState) => ({ - dynamicSettingsState: selectDynamicSettings(state), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - dispatchGetDynamicSettings: () => { - return dispatch(getDynamicSettings({})); - }, - dispatchSetDynamicSettings: (settings: DynamicSettings) => { - return dispatch(setDynamicSettings(settings)); - }, -}); - -export const SettingsPage = connect(mapStateToProps, mapDispatchToProps)(SettingsPageComponent); diff --git a/x-pack/package.json b/x-pack/package.json index b2ec4c3dc3f6f..a4fdb17f52fe5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -179,7 +179,7 @@ "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", "@babel/runtime": "^7.9.2", - "@elastic/apm-rum-react": "^0.3.2", + "@elastic/apm-rum-react": "^1.1.1", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", "@elastic/eui": "21.0.1", diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts index 1acb6c563801c..cc07a0b90330d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts @@ -147,7 +147,14 @@ class ServiceNow { comments: Comment[], field: string ): Promise { - const res = await Promise.all(comments.map(c => this.createComment(incidentId, c, field))); + // Create comments sequentially. + const promises = comments.reduce(async (prevPromise, currentComment) => { + const totalComments = await prevPromise; + const res = await this.createComment(incidentId, currentComment, field); + return [...totalComments, res]; + }, Promise.resolve([] as CommentResponse[])); + + const res = await promises; return res; } diff --git a/x-pack/plugins/apm/common/ml_job_constants.test.ts b/x-pack/plugins/apm/common/ml_job_constants.test.ts index cea66f31303be..2aa50a305f7c8 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.test.ts +++ b/x-pack/plugins/apm/common/ml_job_constants.test.ts @@ -21,6 +21,12 @@ describe('ml_job_constants', () => { expect(getMlJobId('myServiceName', 'myTransactionType')).toBe( 'myservicename-mytransactiontype-high_mean_response_time' ); + expect(getMlJobId('my service name')).toBe( + 'my_service_name-high_mean_response_time' + ); + expect(getMlJobId('my service name', 'my transaction type')).toBe( + 'my_service_name-my_transaction_type-high_mean_response_time' + ); }); it('getMlIndex', () => { diff --git a/x-pack/plugins/apm/common/ml_job_constants.ts b/x-pack/plugins/apm/common/ml_job_constants.ts index 37487d09e4f4c..01f5762e2dc4b 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.ts +++ b/x-pack/plugins/apm/common/ml_job_constants.ts @@ -6,7 +6,7 @@ export function getMlPrefix(serviceName: string, transactionType?: string) { const maybeTransactionType = transactionType ? `${transactionType}-` : ''; - return `${serviceName}-${maybeTransactionType}`.toLowerCase(); + return encodeForMlApi(`${serviceName}-${maybeTransactionType}`); } export function getMlJobId(serviceName: string, transactionType?: string) { @@ -16,3 +16,7 @@ export function getMlJobId(serviceName: string, transactionType?: string) { export function getMlIndex(serviceName: string, transactionType?: string) { return `.ml-anomalies-${getMlJobId(serviceName, transactionType)}`; } + +export function encodeForMlApi(value: string) { + return value.replace(/\s+/g, '_').toLowerCase(); +} diff --git a/x-pack/plugins/canvas/server/saved_objects/custom_element.ts b/x-pack/plugins/canvas/server/saved_objects/custom_element.ts index dadead0263be1..14223455cbc21 100644 --- a/x-pack/plugins/canvas/server/saved_objects/custom_element.ts +++ b/x-pack/plugins/canvas/server/saved_objects/custom_element.ts @@ -10,7 +10,7 @@ import { CUSTOM_ELEMENT_TYPE } from '../../../../legacy/plugins/canvas/common/li export const customElementType: SavedObjectsType = { name: CUSTOM_ELEMENT_TYPE, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { dynamic: false, properties: { diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad.ts b/x-pack/plugins/canvas/server/saved_objects/workpad.ts index e83ba9720b43a..918f4bf991076 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad.ts @@ -11,7 +11,7 @@ import { removeAttributesId } from './migrations/remove_attributes_id'; export const workpadType: SavedObjectsType = { name: CANVAS_TYPE, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { dynamic: false, properties: { diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.ts index e947118a39e8e..ac32b20541a9c 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.ts @@ -40,6 +40,10 @@ export function initGetCaseApi({ caseService, router }: RouteDeps) { const theComments = await caseService.getAllCaseComments({ client, caseId: request.params.case_id, + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, }); return response.ok({ diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts index a4c5dab0feeb7..cc2b1e74b38c4 100644 --- a/x-pack/plugins/case/server/saved_object_types/cases.ts +++ b/x-pack/plugins/case/server/saved_object_types/cases.ts @@ -11,7 +11,7 @@ export const CASE_SAVED_OBJECT = 'cases'; export const caseSavedObjectType: SavedObjectsType = { name: CASE_SAVED_OBJECT, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { closed_at: { diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts index 73b1852bafe58..8b69f272d5b0d 100644 --- a/x-pack/plugins/case/server/saved_object_types/comments.ts +++ b/x-pack/plugins/case/server/saved_object_types/comments.ts @@ -11,7 +11,7 @@ export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments'; export const caseCommentSavedObjectType: SavedObjectsType = { name: CASE_COMMENT_SAVED_OBJECT, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { comment: { diff --git a/x-pack/plugins/case/server/saved_object_types/configure.ts b/x-pack/plugins/case/server/saved_object_types/configure.ts index d66c38b6ea8ff..d6bc3c9f2e227 100644 --- a/x-pack/plugins/case/server/saved_object_types/configure.ts +++ b/x-pack/plugins/case/server/saved_object_types/configure.ts @@ -11,7 +11,7 @@ export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure'; export const caseConfigureSavedObjectType: SavedObjectsType = { name: CASE_CONFIGURE_SAVED_OBJECT, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { created_at: { diff --git a/x-pack/plugins/case/server/saved_object_types/user_actions.ts b/x-pack/plugins/case/server/saved_object_types/user_actions.ts index b61bfafc3b33c..826c6907efea6 100644 --- a/x-pack/plugins/case/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/case/server/saved_object_types/user_actions.ts @@ -11,7 +11,7 @@ export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions'; export const caseUserActionSavedObjectType: SavedObjectsType = { name: CASE_USER_ACTION_SAVED_OBJECT, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { action_field: { diff --git a/x-pack/plugins/endpoint/common/schema/index_pattern.ts b/x-pack/plugins/endpoint/common/schema/index_pattern.ts new file mode 100644 index 0000000000000..2809004f88c6e --- /dev/null +++ b/x-pack/plugins/endpoint/common/schema/index_pattern.ts @@ -0,0 +1,9 @@ +/* + * 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'; + +export const indexPatternGetParamsSchema = schema.object({ datasetPath: schema.string() }); diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 7143f07d8c702..49f8ebbd580d8 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -7,6 +7,7 @@ import { SearchResponse } from 'elasticsearch'; import { TypeOf } from '@kbn/config-schema'; import { alertingIndexGetQuerySchema } from './schema/alert_index'; +import { indexPatternGetParamsSchema } from './schema/index_pattern'; import { Datasource, NewDatasource } from '../../ingest_manager/common'; /** @@ -33,9 +34,9 @@ export type Direction = 'asc' | 'desc'; export class EndpointAppConstants { static BASE_API_URL = '/api/endpoint'; - static ENDPOINT_INDEX_NAME = 'endpoint-agent*'; + static INDEX_PATTERN_ROUTE = `${EndpointAppConstants.BASE_API_URL}/index_pattern`; static ALERT_INDEX_NAME = 'events-endpoint-1'; - static EVENT_INDEX_NAME = 'events-endpoint-*'; + static EVENT_DATASET = 'events'; static DEFAULT_TOTAL_HITS = 10000; /** * Legacy events are stored in indices with endgame-* prefix @@ -446,6 +447,11 @@ export type AlertingIndexGetQueryInput = KbnConfigSchemaInputTypeOf< */ export type AlertingIndexGetQueryResult = TypeOf; +/** + * Result of the validated params when handling an index pattern request. + */ +export type IndexPatternGetParamsResult = TypeOf; + /** * Endpoint Policy configuration */ diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx index 7cb1031ef9a09..639b1f7252d7f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx @@ -12,6 +12,7 @@ import { coreMock } from '../../../../../../../src/core/public/mocks'; import { EndpointPluginStartDependencies } from '../../../plugin'; import { depsStartMock } from './dependencies_start_mock'; import { AppRootProvider } from '../view/app_root_provider'; +import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../store/test_utils'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -23,6 +24,7 @@ export interface AppContextTestRender { history: ReturnType; coreStart: ReturnType; depsStart: EndpointPluginStartDependencies; + middlewareSpy: MiddlewareActionSpyHelper; /** * A wrapper around `AppRootContext` component. Uses the mocked modules as input to the * `AppRootContext` @@ -45,7 +47,12 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { const history = createMemoryHistory(); const coreStart = coreMock.createStart({ basePath: '/mock' }); const depsStart = depsStartMock(); - const store = appStoreFactory({ coreStart, depsStart }); + const middlewareSpy = createSpyMiddleware(); + const store = appStoreFactory({ + coreStart, + depsStart, + additionalMiddleware: [middlewareSpy.actionSpyMiddleware], + }); const AppWrapper: React.FunctionComponent<{ children: React.ReactElement }> = ({ children }) => ( {children} @@ -64,6 +71,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { history, coreStart, depsStart, + middlewareSpy, AppWrapper, render, }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts index 2c6ebf52189f5..52d72c6631443 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts @@ -15,10 +15,14 @@ import { EndpointAppConstants } from '../../../../../common/types'; export const alertMiddlewareFactory: MiddlewareFactory = (coreStart, depsStart) => { async function fetchIndexPatterns(): Promise { const { indexPatterns } = depsStart.data; - const indexName = EndpointAppConstants.ALERT_INDEX_NAME; - const fields = await indexPatterns.getFieldsForWildcard({ pattern: indexName }); + const eventsPattern: { indexPattern: string } = await coreStart.http.get( + `${EndpointAppConstants.INDEX_PATTERN_ROUTE}/${EndpointAppConstants.EVENT_DATASET}` + ); + const fields = await indexPatterns.getFieldsForWildcard({ + pattern: eventsPattern.indexPattern, + }); const indexPattern: IIndexPattern = { - title: indexName, + title: eventsPattern.indexPattern, fields, }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts index 35bf5d0616878..03cdba8505800 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts @@ -37,7 +37,7 @@ export const uiQueryParams: ( // Removes the `?` from the beginning of query string if it exists const query = querystring.parse(location.search.slice(1)); - const keys: Array = ['selected_host']; + const keys: Array = ['selected_host', 'show']; for (const key of keys) { const value = query[key]; @@ -58,3 +58,11 @@ export const hasSelectedHost: (state: Immutable) => boolean = cre return selectedHost !== undefined; } ); + +/** What policy details panel view to show */ +export const showView: (state: HostListState) => 'policy_response' | 'details' = createSelector( + uiQueryParams, + searchParams => { + return searchParams.show === 'policy_response' ? 'policy_response' : 'details'; + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts index efa79b163d3b6..60758f0f5fea0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -19,7 +19,7 @@ import { alertMiddlewareFactory } from './alerts/middleware'; import { hostMiddlewareFactory } from './hosts'; import { policyListMiddlewareFactory } from './policy_list'; import { policyDetailsMiddlewareFactory } from './policy_details'; -import { GlobalState } from '../types'; +import { GlobalState, MiddlewareFactory } from '../types'; import { AppAction } from './action'; import { EndpointPluginStartDependencies } from '../../../plugin'; @@ -62,10 +62,15 @@ export const appStoreFactory: (middlewareDeps?: { * Give middleware access to plugin start dependencies. */ depsStart: EndpointPluginStartDependencies; + /** + * Any additional Redux Middlewares + * (should only be used for testing - example: to inject the action spy middleware) + */ + additionalMiddleware?: Array>; }) => Store = middlewareDeps => { let middleware; if (middlewareDeps) { - const { coreStart, depsStart } = middlewareDeps; + const { coreStart, depsStart, additionalMiddleware = [] } = middlewareDeps; middleware = composeWithReduxDevTools( applyMiddleware( substateMiddlewareFactory( @@ -83,7 +88,9 @@ export const appStoreFactory: (middlewareDeps?: { substateMiddlewareFactory( globalState => globalState.alertList, alertMiddlewareFactory(coreStart, depsStart) - ) + ), + // Additional Middleware should go last + ...additionalMiddleware ) ); } else { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts index 97a2b65fb65f8..69b11fb3c1f0e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts @@ -12,13 +12,10 @@ import { policyListMiddlewareFactory } from './middleware'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { isOnPolicyListPage, selectIsLoading, urlSearchParams } from './selectors'; import { DepsStartMock, depsStartMock } from '../../mocks'; -import { - createSpyMiddleware, - MiddlewareActionSpyHelper, - setPolicyListApiMockImplementation, -} from './test_mock_utils'; +import { setPolicyListApiMockImplementation } from './test_mock_utils'; import { INGEST_API_DATASOURCES } from './services/ingest'; import { Immutable } from '../../../../../common/types'; +import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../test_utils'; describe('policy list store concerns', () => { let fakeCoreStart: ReturnType; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts index 20d5a637182d2..a1788b8f8021d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts @@ -5,10 +5,9 @@ */ import { HttpStart } from 'kibana/public'; -import { Dispatch } from 'redux'; import { INGEST_API_DATASOURCES } from './services/ingest'; import { EndpointDocGenerator } from '../../../../../common/generate_data'; -import { AppAction, GetPolicyListResponse, GlobalState, MiddlewareFactory } from '../../types'; +import { GetPolicyListResponse } from '../../types'; const generator = new EndpointDocGenerator('policy-list'); @@ -37,115 +36,3 @@ export const setPolicyListApiMockImplementation = ( return Promise.reject(new Error(`MOCK: unknown policy list api: ${path}`)); }); }; - -/** - * Utilities for testing Redux middleware - */ -export interface MiddlewareActionSpyHelper { - /** - * Returns a promise that is fulfilled when the given action is dispatched or a timeout occurs. - * The use of this method instead of a `sleep()` type of delay should avoid test case instability - * especially when run in a CI environment. - * - * @param actionType - */ - waitForAction: (actionType: AppAction['type']) => Promise; - /** - * A property holding the information around the calls that were processed by the internal - * `actionSpyMiddlware`. This property holds the information typically found in Jets's mocked - * function `mock` property - [see here for more information](https://jestjs.io/docs/en/mock-functions#mock-property) - * - * **Note**: this property will only be set **after* the `actionSpyMiddlware` has been - * initialized (ex. via `createStore()`. Attempting to reference this property before that time - * will throw an error. - * Also - do not hold on to references to this property value if `jest.clearAllMocks()` or - * `jest.resetAllMocks()` is called between usages of the value. - */ - dispatchSpy: jest.Mock>['mock']; - /** - * Redux middleware that enables spying on the action that are dispatched through the store - */ - actionSpyMiddleware: ReturnType>; -} - -/** - * Creates a new instance of middleware action helpers - * Note: in most cases (testing concern specific middleware) this function should be given - * the state type definition, else, the global state will be used. - * - * @example - * // Use in Policy List middleware testing - * const middlewareSpyUtils = createSpyMiddleware(); - * store = createStore( - * policyListReducer, - * applyMiddleware( - * policyListMiddlewareFactory(fakeCoreStart, depsStart), - * middlewareSpyUtils.actionSpyMiddleware - * ) - * ); - * // Reference `dispatchSpy` ONLY after creating the store that includes `actionSpyMiddleware` - * const { waitForAction, dispatchSpy } = middlewareSpyUtils; - * // - * // later in test - * // - * it('...', async () => { - * //... - * await waitForAction('serverReturnedPolicyListData'); - * // do assertions - * // or check how action was called - * expect(dispatchSpy.calls.length).toBe(2) - * }); - */ -export const createSpyMiddleware = (): MiddlewareActionSpyHelper => { - type ActionWatcher = (action: AppAction) => void; - - const watchers = new Set(); - let spyDispatch: jest.Mock>; - - return { - waitForAction: async (actionType: string) => { - // Error is defined here so that we get a better stack trace that points to the test from where it was used - const err = new Error(`action '${actionType}' was not dispatched within the allocated time`); - - await new Promise((resolve, reject) => { - const watch: ActionWatcher = action => { - if (action.type === actionType) { - watchers.delete(watch); - clearTimeout(timeout); - resolve(); - } - }; - - // We timeout before jest's default 5s, so that a better error stack is returned - const timeout = setTimeout(() => { - watchers.delete(watch); - reject(err); - }, 4500); - watchers.add(watch); - }); - }, - - get dispatchSpy() { - if (!spyDispatch) { - throw new Error( - 'Spy Middleware has not been initialized. Access this property only after using `actionSpyMiddleware` in a redux store' - ); - } - return spyDispatch.mock; - }, - - actionSpyMiddleware: api => { - return next => { - spyDispatch = jest.fn(action => { - next(action); - // loop through the list of watcher (if any) and call them with this action - for (const watch of watchers) { - watch(action); - } - return action; - }); - return spyDispatch; - }; - }, - }; -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/test_utils.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/test_utils.ts new file mode 100644 index 0000000000000..99e14cef73e8b --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/test_utils.ts @@ -0,0 +1,126 @@ +/* + * 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 { Dispatch } from 'redux'; +import { AppAction, GlobalState, MiddlewareFactory } from '../types'; + +/** + * Utilities for testing Redux middleware + */ +export interface MiddlewareActionSpyHelper { + /** + * Returns a promise that is fulfilled when the given action is dispatched or a timeout occurs. + * The `action` will given to the promise `resolve` thus allowing for checks to be done. + * The use of this method instead of a `sleep()` type of delay should avoid test case instability + * especially when run in a CI environment. + * + * @param actionType + */ + waitForAction: (actionType: T) => Promise; + /** + * A property holding the information around the calls that were processed by the internal + * `actionSpyMiddelware`. This property holds the information typically found in Jets's mocked + * function `mock` property - [see here for more information](https://jestjs.io/docs/en/mock-functions#mock-property) + * + * **Note**: this property will only be set **after* the `actionSpyMiddlware` has been + * initialized (ex. via `createStore()`. Attempting to reference this property before that time + * will throw an error. + * Also - do not hold on to references to this property value if `jest.clearAllMocks()` or + * `jest.resetAllMocks()` is called between usages of the value. + */ + dispatchSpy: jest.Mock>['mock']; + /** + * Redux middleware that enables spying on the action that are dispatched through the store + */ + actionSpyMiddleware: ReturnType>; +} + +/** + * Creates a new instance of middleware action helpers + * Note: in most cases (testing concern specific middleware) this function should be given + * the state type definition, else, the global state will be used. + * + * @example + * // Use in Policy List middleware testing + * const middlewareSpyUtils = createSpyMiddleware(); + * store = createStore( + * policyListReducer, + * applyMiddleware( + * policyListMiddlewareFactory(fakeCoreStart, depsStart), + * middlewareSpyUtils.actionSpyMiddleware + * ) + * ); + * // Reference `dispatchSpy` ONLY after creating the store that includes `actionSpyMiddleware` + * const { waitForAction, dispatchSpy } = middlewareSpyUtils; + * // + * // later in test + * // + * it('...', async () => { + * //... + * await waitForAction('serverReturnedPolicyListData'); + * // do assertions + * // or check how action was called + * expect(dispatchSpy.calls.length).toBe(2) + * }); + */ +export const createSpyMiddleware = < + S = GlobalState, + A extends AppAction = AppAction +>(): MiddlewareActionSpyHelper => { + type ActionWatcher = (action: A) => void; + + const watchers = new Set(); + let spyDispatch: jest.Mock>; + + return { + waitForAction: async actionType => { + type ResolvedAction = A extends { type: typeof actionType } ? A : never; + + // Error is defined here so that we get a better stack trace that points to the test from where it was used + const err = new Error(`action '${actionType}' was not dispatched within the allocated time`); + + return new Promise((resolve, reject) => { + const watch: ActionWatcher = action => { + if (action.type === actionType) { + watchers.delete(watch); + clearTimeout(timeout); + resolve(action as ResolvedAction); + } + }; + + // We timeout before jest's default 5s, so that a better error stack is returned + const timeout = setTimeout(() => { + watchers.delete(watch); + reject(err); + }, 4500); + watchers.add(watch); + }); + }, + + get dispatchSpy() { + if (!spyDispatch) { + throw new Error( + 'Spy Middleware has not been initialized. Access this property only after using `actionSpyMiddleware` in a redux store' + ); + } + return spyDispatch.mock; + }, + + actionSpyMiddleware: api => { + return next => { + spyDispatch = jest.fn(action => { + next(action); + // loop through the list of watcher (if any) and call them with this action + for (const watch of watchers) { + watch(action); + } + return action; + }); + return spyDispatch; + }; + }, + }; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 7aca94d3e9c7c..59cd8f806e5b0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -52,6 +52,7 @@ export interface HostListPagination { } export interface HostIndexUIQueryParams { selected_host?: string; + show?: string; } export interface ServerApiError { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/__snapshots__/page_view.test.tsx.snap b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/__snapshots__/page_view.test.tsx.snap index dfc69fc46ebdc..36b602a1e6784 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/__snapshots__/page_view.test.tsx.snap +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/__snapshots__/page_view.test.tsx.snap @@ -7,6 +7,7 @@ exports[`PageView component should display body header custom element 1`] = ` .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -97,6 +98,7 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] = .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -190,6 +192,7 @@ exports[`PageView component should display header left and right 1`] = ` .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -298,6 +301,7 @@ exports[`PageView component should display only body if not header props used 1` .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -365,6 +369,7 @@ exports[`PageView component should display only header left 1`] = ` .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -462,6 +467,7 @@ exports[`PageView component should display only header right but include an empt .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -556,6 +562,7 @@ exports[`PageView component should pass through EuiPage props 1`] = ` .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { @@ -640,6 +647,7 @@ exports[`PageView component should use custom element for header left and not wr .c0.endpoint--isListView .endpoint-header { padding: 24px; + margin-bottom: 0; } .c0.endpoint--isListView .endpoint-page-content { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/page_view.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/page_view.tsx index 561d671e18e07..6da352b68f890 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/page_view.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/page_view.tsx @@ -25,6 +25,7 @@ const StyledEuiPage = styled(EuiPage)` .endpoint-header { padding: ${props => props.theme.eui.euiSizeL}; + margin-bottom: 0; } .endpoint-page-content { border-left: none; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx new file mode 100644 index 0000000000000..26f2203790a9e --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx @@ -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 React, { memo } from 'react'; +import { EuiFlyoutHeader, CommonProps, EuiButtonEmpty } from '@elastic/eui'; +import styled from 'styled-components'; + +export type FlyoutSubHeaderProps = CommonProps & { + children: React.ReactNode; + backButton?: { + title: string; + onClick: (event: React.MouseEvent) => void; + href?: string; + }; +}; + +const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` + padding: ${props => props.theme.eui.paddingSizes.s}; + + &.hasButtons { + .buttons { + padding-bottom: ${props => props.theme.eui.paddingSizes.s}; + } + + .back-button-content { + padding-left: 0; + &-text { + margin-left: 0; + } + } + } + + .flyout-content { + padding-left: ${props => props.theme.eui.paddingSizes.m}; + } +`; + +const BUTTON_CONTENT_PROPS = Object.freeze({ className: 'back-button-content' }); +const BUTTON_TEXT_PROPS = Object.freeze({ className: 'back-button-content-text' }); + +/** + * A Eui Flyout Header component that has its styles adjusted to display a panel sub-header. + * Component also provides a way to display a "back" button above the header title. + */ +export const FlyoutSubHeader = memo( + ({ children, backButton, ...otherProps }) => { + return ( + + {backButton && ( +
+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + {backButton?.title} + +
+ )} +
{children}
+
+ ); + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx similarity index 53% rename from x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx rename to x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx index f51349b24933a..32c69426b03f3 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx @@ -4,31 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, memo, useEffect } from 'react'; +import styled from 'styled-components'; import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, EuiDescriptionList, - EuiLoadingContent, - EuiHorizontalRule, EuiHealth, - EuiSpacer, + EuiHorizontalRule, + EuiLink, EuiListGroup, EuiListGroupItem, } from '@elastic/eui'; -import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; +import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { HostMetadata } from '../../../../../common/types'; -import { useHostListSelector } from './hooks'; -import { urlFromQueryParams } from './url_from_query_params'; -import { FormattedDateAndTime } from '../formatted_date_time'; -import { uiQueryParams, detailsData, detailsError } from './../../store/hosts/selectors'; -import { LinkToApp } from '../components/link_to_app'; +import { i18n } from '@kbn/i18n'; +import { useHistory } from 'react-router-dom'; +import { HostMetadata } from '../../../../../../common/types'; +import { FormattedDateAndTime } from '../../formatted_date_time'; +import { LinkToApp } from '../../components/link_to_app'; +import { useHostListSelector, useHostLogsUrl } from '../hooks'; +import { urlFromQueryParams } from '../url_from_query_params'; +import { uiQueryParams } from '../../../store/hosts/selectors'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -37,8 +31,10 @@ const HostIds = styled(EuiListGroupItem)` } `; -const HostDetails = memo(({ details }: { details: HostMetadata }) => { +export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const { appId, appPath, url } = useHostLogsUrl(details.host.id); + const queryParams = useHostListSelector(uiQueryParams); + const history = useHistory(); const detailsResultsUpper = useMemo(() => { return [ { @@ -62,6 +58,14 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => { ]; }, [details]); + const policyResponseUri = useMemo(() => { + return urlFromQueryParams({ + ...queryParams, + selected_host: details.host.id, + show: 'policy_response', + }); + }, [details.host.id, queryParams]); + const detailsResultsLower = useMemo(() => { return [ { @@ -74,7 +78,24 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => { title: i18n.translate('xpack.endpoint.host.details.policyStatus', { defaultMessage: 'Policy Status', }), - description: active, + description: ( + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { + ev.preventDefault(); + history.push(policyResponseUri); + }} + > + + + + ), }, { title: i18n.translate('xpack.endpoint.host.details.ipAddress', { @@ -101,7 +122,15 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => { description: details.agent.version, }, ]; - }, [details.agent.version, details.endpoint.policy.id, details.host.hostname, details.host.ip]); + }, [ + details.agent.version, + details.endpoint.policy.id, + details.host.hostname, + details.host.ip, + history, + policyResponseUri, + ]); + return ( <> { ); }); - -export const HostDetailsFlyout = () => { - const history = useHistory(); - const { notifications } = useKibana(); - const queryParams = useHostListSelector(uiQueryParams); - const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; - const details = useHostListSelector(detailsData); - const error = useHostListSelector(detailsError); - - const handleFlyoutClose = useCallback(() => { - history.push(urlFromQueryParams(queryParamsWithoutSelectedHost)); - }, [history, queryParamsWithoutSelectedHost]); - - useEffect(() => { - if (error !== undefined) { - notifications.toasts.danger({ - title: ( - - ), - body: ( - - ), - toastLifeTimeMs: 10000, - }); - } - }, [error, notifications.toasts]); - - return ( - - - -

- {details === undefined ? : details.host.hostname} -

-
-
- - {details === undefined ? ( - <> - - - ) : ( - - )} - -
- ); -}; - -const useHostLogsUrl = (hostId: string): { url: string; appId: string; appPath: string } => { - const { services } = useKibana(); - return useMemo(() => { - const appPath = `/stream?logFilter=(expression:'host.id:${hostId}',kind:kuery)`; - return { - url: `${services.application.getUrlForApp('logs')}${appPath}`, - appId: 'logs', - appPath, - }; - }, [hostId, services.application]); -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx new file mode 100644 index 0000000000000..a41d4a968f177 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx @@ -0,0 +1,134 @@ +/* + * 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, { useCallback, useEffect, memo, useMemo } from 'react'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiLoadingContent, + EuiSpacer, +} from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { useHostListSelector } from '../hooks'; +import { urlFromQueryParams } from '../url_from_query_params'; +import { uiQueryParams, detailsData, detailsError, showView } from '../../../store/hosts/selectors'; +import { HostDetails } from './host_details'; +import { PolicyResponse } from './policy_response'; +import { HostMetadata } from '../../../../../../common/types'; +import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header'; + +export const HostDetailsFlyout = memo(() => { + const history = useHistory(); + const { notifications } = useKibana(); + const queryParams = useHostListSelector(uiQueryParams); + const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; + const details = useHostListSelector(detailsData); + const error = useHostListSelector(detailsError); + const show = useHostListSelector(showView); + + const handleFlyoutClose = useCallback(() => { + history.push(urlFromQueryParams(queryParamsWithoutSelectedHost)); + }, [history, queryParamsWithoutSelectedHost]); + + useEffect(() => { + if (error !== undefined) { + notifications.toasts.danger({ + title: ( + + ), + body: ( + + ), + toastLifeTimeMs: 10000, + }); + } + }, [error, notifications.toasts]); + + return ( + + + +

+ {details === undefined ? : details.host.hostname} +

+
+
+ {details === undefined ? ( + <> + + + + + ) : ( + <> + {show === 'details' && ( + <> + + + + + )} + {show === 'policy_response' && } + + )} +
+ ); +}); + +const PolicyResponseFlyoutPanel = memo<{ + hostMeta: HostMetadata; +}>(({ hostMeta }) => { + const history = useHistory(); + const { show, ...queryParams } = useHostListSelector(uiQueryParams); + const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => { + const detailsUri = urlFromQueryParams({ + ...queryParams, + selected_host: hostMeta.host.id, + }); + return { + title: i18n.translate('xpack.endpoint.host.policyResponse.backLinkTitle', { + defaultMessage: 'Endpoint Details', + }), + href: '?' + detailsUri.search, + onClick: ev => { + ev.preventDefault(); + history.push(detailsUri); + }, + }; + }, [history, hostMeta.host.id, queryParams]); + + return ( + <> + + +

+ +

+
+
+ + + + + ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx new file mode 100644 index 0000000000000..eacb6a52d3184 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/policy_response.tsx @@ -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 React, { memo } from 'react'; + +export const PolicyResponse = memo(() => { + return
Policy Status to be displayed here soon.
; +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts index 99a0073f46c74..7eb51f3a7b294 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts @@ -5,10 +5,28 @@ */ import { useSelector } from 'react-redux'; +import { useMemo } from 'react'; import { GlobalState, HostListState } from '../../types'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; export function useHostListSelector(selector: (state: HostListState) => TSelected) { return useSelector(function(state: GlobalState) { return selector(state.hostList); }); } + +/** + * Returns an object that contains Kibana Logs app and URL information for a given host id + * @param hostId + */ +export const useHostLogsUrl = (hostId: string): { url: string; appId: string; appPath: string } => { + const { services } = useKibana(); + return useMemo(() => { + const appPath = `/stream?logFilter=(expression:'host.id:${hostId}',kind:kuery)`; + return { + url: `${services.application.getUrlForApp('logs')}${appPath}`, + appId: 'logs', + appPath, + }; + }, [hostId, services.application]); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index d2d0ad40b025f..88416b577ed0c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -21,10 +21,11 @@ describe('when on the hosts page', () => { let history: AppContextTestRender['history']; let store: AppContextTestRender['store']; let coreStart: AppContextTestRender['coreStart']; + let middlewareSpy: AppContextTestRender['middlewareSpy']; beforeEach(async () => { const mockedContext = createAppRootMockRenderer(); - ({ history, store, coreStart } = mockedContext); + ({ history, store, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(); }); @@ -132,6 +133,25 @@ describe('when on the hosts page', () => { expect(flyout).not.toBeNull(); }); }); + it('should display policy status value as a link', async () => { + const renderResult = render(); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusLink).not.toBeNull(); + expect(policyStatusLink.textContent).toEqual('Successful'); + expect(policyStatusLink.getAttribute('href')).toEqual( + '?selected_host=1&show=policy_response' + ); + }); + it('should update the URL when policy status link is clicked', async () => { + const renderResult = render(); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); + reactTestingLibrary.act(() => { + fireEvent.click(policyStatusLink); + }); + const changedUrlAction = await userChangedUrlChecker; + expect(changedUrlAction.payload.search).toEqual('?selected_host=1&show=policy_response'); + }); it('should include the link to logs', async () => { const renderResult = render(); const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); @@ -154,5 +174,48 @@ describe('when on the hosts page', () => { expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); }); }); + describe('when showing host Policy Response', () => { + let renderResult: ReturnType; + beforeEach(async () => { + renderResult = render(); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); + reactTestingLibrary.act(() => { + fireEvent.click(policyStatusLink); + }); + await userChangedUrlChecker; + }); + it('should hide the host details panel', async () => { + const hostDetailsFlyout = await renderResult.queryByTestId('hostDetailsFlyoutBody'); + expect(hostDetailsFlyout).toBeNull(); + }); + it('should display policy response sub-panel', async () => { + expect( + await renderResult.findByTestId('hostDetailsPolicyResponseFlyoutHeader') + ).not.toBeNull(); + expect( + await renderResult.findByTestId('hostDetailsPolicyResponseFlyoutBody') + ).not.toBeNull(); + }); + it('should include the sub-panel title', async () => { + expect( + (await renderResult.findByTestId('hostDetailsPolicyResponseFlyoutTitle')).textContent + ).toBe('Policy Response'); + }); + it('should include the back to details link', async () => { + const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); + expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); + expect(subHeaderBackLink.getAttribute('href')).toBe('?selected_host=1'); + }); + it('should update URL when back to details link is clicked', async () => { + const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); + reactTestingLibrary.act(() => { + fireEvent.click(subHeaderBackLink); + }); + const changedUrlAction = await userChangedUrlChecker; + expect(changedUrlAction.payload.search).toEqual('?selected_host=1'); + }); + }); }); }); diff --git a/x-pack/plugins/endpoint/scripts/resolver_generator.ts b/x-pack/plugins/endpoint/scripts/resolver_generator.ts index 333846bde6ce4..dd9e591f4b034 100644 --- a/x-pack/plugins/endpoint/scripts/resolver_generator.ts +++ b/x-pack/plugins/endpoint/scripts/resolver_generator.ts @@ -41,7 +41,7 @@ async function main() { metadataIndex: { alias: 'mi', describe: 'index to store host metadata in', - default: 'endpoint-agent-1', + default: 'metrics-endpoint-default-1', type: 'string', }, auth: { diff --git a/x-pack/plugins/endpoint/server/index_pattern.ts b/x-pack/plugins/endpoint/server/index_pattern.ts new file mode 100644 index 0000000000000..ea612bfd75441 --- /dev/null +++ b/x-pack/plugins/endpoint/server/index_pattern.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger, LoggerFactory, RequestHandlerContext } from 'kibana/server'; +import { ESIndexPatternService } from '../../ingest_manager/server'; +import { EndpointAppConstants } from '../common/types'; + +export interface IndexPatternRetriever { + getIndexPattern(ctx: RequestHandlerContext, datasetPath: string): Promise; + getEventIndexPattern(ctx: RequestHandlerContext): Promise; + getMetadataIndexPattern(ctx: RequestHandlerContext): Promise; +} + +/** + * This class is used to retrieve an index pattern. It should be used in the server side code whenever + * an index pattern is needed to query data within ES. The index pattern is constructed by the Ingest Manager + * based on the contents of the Endpoint Package in the Package Registry. + */ +export class IngestIndexPatternRetriever implements IndexPatternRetriever { + private static endpointPackageName = 'endpoint'; + private static metadataDataset = 'metadata'; + private readonly log: Logger; + constructor(private readonly service: ESIndexPatternService, loggerFactory: LoggerFactory) { + this.log = loggerFactory.get('index-pattern-retriever'); + } + + /** + * Retrieves the index pattern for querying events within elasticsearch. + * + * @param ctx a RequestHandlerContext from a route handler + * @returns a string representing the index pattern (e.g. `events-endpoint-*`) + */ + async getEventIndexPattern(ctx: RequestHandlerContext) { + return await this.getIndexPattern(ctx, EndpointAppConstants.EVENT_DATASET); + } + + /** + * Retrieves the index pattern for querying endpoint metadata within elasticsearch. + * + * @param ctx a RequestHandlerContext from a route handler + * @returns a string representing the index pattern (e.g. `metrics-endpoint-*`) + */ + async getMetadataIndexPattern(ctx: RequestHandlerContext) { + return await this.getIndexPattern(ctx, IngestIndexPatternRetriever.metadataDataset); + } + + /** + * Retrieves the index pattern for a specific dataset for querying endpoint data. + * + * @param ctx a RequestHandlerContext from a route handler + * @param datasetPath a string of the path being used for a dataset within the Endpoint Package + * (e.g. `events`, `metadata`) + * @returns a string representing the index pattern (e.g. `metrics-endpoint-*`) + */ + async getIndexPattern(ctx: RequestHandlerContext, datasetPath: string) { + try { + const pattern = await this.service.getESIndexPattern( + ctx.core.savedObjects.client, + IngestIndexPatternRetriever.endpointPackageName, + datasetPath + ); + + if (!pattern) { + const msg = `Unable to retrieve the index pattern for dataset: ${datasetPath}`; + this.log.warn(msg); + throw new Error(msg); + } + return pattern; + } catch (error) { + const errMsg = `Error occurred while retrieving pattern for: ${datasetPath} error: ${error}`; + this.log.warn(errMsg); + throw new Error(errMsg); + } + } +} diff --git a/x-pack/plugins/endpoint/server/mocks.ts b/x-pack/plugins/endpoint/server/mocks.ts new file mode 100644 index 0000000000000..903aa19cd8843 --- /dev/null +++ b/x-pack/plugins/endpoint/server/mocks.ts @@ -0,0 +1,44 @@ +/* + * 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. + */ + +/** + * Creates a mock IndexPatternRetriever for use in tests. + * + * @param indexPattern a string index pattern to return when any of the mock's public methods are called. + * @returns the same string passed in via `indexPattern` + */ +export const createMockIndexPatternRetriever = (indexPattern: string) => { + const mockGetFunc = jest.fn().mockResolvedValue(indexPattern); + return { + getIndexPattern: mockGetFunc, + getEventIndexPattern: mockGetFunc, + getMetadataIndexPattern: mockGetFunc, + }; +}; + +export const MetadataIndexPattern = 'metrics-endpoint-*'; + +/** + * Creates a mock IndexPatternRetriever for use in tests that returns `metrics-endpoint-*` + */ +export const createMockMetadataIndexPatternRetriever = () => { + return createMockIndexPatternRetriever(MetadataIndexPattern); +}; + +/** + * Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's + * ESIndexPatternService. + * + * @param indexPattern a string index pattern to return when called by a test + * @returns the same value as `indexPattern` parameter + */ +export const createMockIndexPatternService = (indexPattern: string) => { + return { + esIndexPatternService: { + getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), + }, + }; +}; diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts index 7dd878d579043..8d55e64f16dcf 100644 --- a/x-pack/plugins/endpoint/server/plugin.test.ts +++ b/x-pack/plugins/endpoint/server/plugin.test.ts @@ -7,6 +7,7 @@ import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { PluginSetupContract } from '../../features/server'; +import { createMockIndexPatternService } from './mocks'; describe('test endpoint plugin', () => { let plugin: EndpointPlugin; @@ -28,7 +29,10 @@ describe('test endpoint plugin', () => { getFeaturesUICapabilities: jest.fn(), registerLegacyAPI: jest.fn(), }; - mockedEndpointPluginSetupDependencies = { features: mockedPluginSetupContract }; + mockedEndpointPluginSetupDependencies = { + features: mockedPluginSetupContract, + ingestManager: createMockIndexPatternService(''), + }; }); it('test properly setup plugin', async () => { diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index d3a399124124f..6a42014e91130 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -6,12 +6,15 @@ import { Plugin, CoreSetup, PluginInitializerContext, Logger } from 'kibana/server'; import { first } from 'rxjs/operators'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; +import { IngestManagerSetupContract } from '../../ingest_manager/server'; import { createConfig$, EndpointConfigType } from './config'; import { EndpointAppContext } from './types'; import { registerAlertRoutes } from './routes/alerts'; import { registerResolverRoutes } from './routes/resolver'; +import { registerIndexPatternRoute } from './routes/index_pattern'; import { registerEndpointRoutes } from './routes/metadata'; +import { IngestIndexPatternRetriever } from './index_pattern'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; @@ -19,6 +22,7 @@ export interface EndpointPluginStartDependencies {} // eslint-disable-line @type export interface EndpointPluginSetupDependencies { features: FeaturesPluginSetupContract; + ingestManager: IngestManagerSetupContract; } export class EndpointPlugin @@ -62,6 +66,10 @@ export class EndpointPlugin }, }); const endpointContext = { + indexPatternRetriever: new IngestIndexPatternRetriever( + plugins.ingestManager.esIndexPatternService, + this.initializerContext.logger + ), logFactory: this.initializerContext.logger, config: (): Promise => { return createConfig$(this.initializerContext) @@ -73,6 +81,7 @@ export class EndpointPlugin registerEndpointRoutes(router, endpointContext); registerResolverRoutes(router, endpointContext); registerAlertRoutes(router, endpointContext); + registerIndexPatternRoute(router, endpointContext); } public start() { diff --git a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts index 5f5e4be4ecd0a..6be7b26898206 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts @@ -12,6 +12,7 @@ import { import { registerAlertRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index'; +import { createMockIndexPatternRetriever } from '../../mocks'; describe('test alerts route', () => { let routerMock: jest.Mocked; @@ -24,6 +25,7 @@ describe('test alerts route', () => { mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); registerAlertRoutes(routerMock, { + indexPatternRetriever: createMockIndexPatternRetriever('events-endpoint-*'), logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts index 0f32deb4fad9b..86e9f55da5697 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts @@ -26,15 +26,18 @@ export const alertDetailsHandlerWrapper = function( id: alertId, })) as GetResponse; + const indexPattern = await endpointAppContext.indexPatternRetriever.getEventIndexPattern(ctx); + const config = await endpointAppContext.config(); const pagination: AlertDetailsPagination = new AlertDetailsPagination( config, ctx, req.params, - response + response, + indexPattern ); - const currentHostInfo = await getHostData(ctx, response._source.host.id); + const currentHostInfo = await getHostData(ctx, response._source.host.id, indexPattern); return res.ok({ body: { diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts index 446d61080e650..d482da03872c6 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts @@ -29,7 +29,8 @@ export class AlertDetailsPagination extends Pagination< config: EndpointConfigType, requestContext: RequestHandlerContext, state: AlertDetailsRequestParams, - data: GetResponse + data: GetResponse, + private readonly indexPattern: string ) { super(config, requestContext, state, data); } @@ -54,7 +55,8 @@ export class AlertDetailsPagination extends Pagination< const response = await searchESForAlerts( this.requestContext.core.elasticsearch.dataClient, - reqData + reqData, + this.indexPattern ); return response; } diff --git a/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts index dc1ce767a715b..74db24c85eab5 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts @@ -97,7 +97,8 @@ function buildSort(query: AlertSearchQuery): AlertSort { * Builds a request body for Elasticsearch, given a set of query params. **/ const buildAlertSearchQuery = async ( - query: AlertSearchQuery + query: AlertSearchQuery, + indexPattern: string ): Promise => { let totalHitsMin: number = EndpointAppConstants.DEFAULT_TOTAL_HITS; @@ -125,7 +126,7 @@ const buildAlertSearchQuery = async ( const reqWrapper: AlertSearchRequestWrapper = { size: query.pageSize, - index: EndpointAppConstants.ALERT_INDEX_NAME, + index: indexPattern, body: reqBody, }; @@ -141,9 +142,10 @@ const buildAlertSearchQuery = async ( **/ export const searchESForAlerts = async ( dataClient: IScopedClusterClient, - query: AlertSearchQuery + query: AlertSearchQuery, + indexPattern: string ): Promise> => { - const reqWrapper = await buildAlertSearchQuery(query); + const reqWrapper = await buildAlertSearchQuery(query, indexPattern); const response = (await dataClient.callAsCurrentUser('search', reqWrapper)) as SearchResponse< AlertEvent >; diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts index 93ec8d7aa3e67..f23dffd13db4f 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts @@ -18,8 +18,13 @@ export const alertListHandlerWrapper = function( res ) => { try { + const indexPattern = await endpointAppContext.indexPatternRetriever.getEventIndexPattern(ctx); const reqData = await getRequestData(req, endpointAppContext); - const response = await searchESForAlerts(ctx.core.elasticsearch.dataClient, reqData); + const response = await searchESForAlerts( + ctx.core.elasticsearch.dataClient, + reqData, + indexPattern + ); const mappedBody = await mapToAlertResultList(ctx, endpointAppContext, reqData, response); return res.ok({ body: mappedBody }); } catch (err) { diff --git a/x-pack/plugins/endpoint/server/routes/index_pattern.ts b/x-pack/plugins/endpoint/server/routes/index_pattern.ts new file mode 100644 index 0000000000000..3b71f6a6957ba --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/index_pattern.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, Logger, RequestHandler } from 'kibana/server'; +import { EndpointAppContext } from '../types'; +import { IndexPatternGetParamsResult, EndpointAppConstants } from '../../common/types'; +import { indexPatternGetParamsSchema } from '../../common/schema/index_pattern'; +import { IndexPatternRetriever } from '../index_pattern'; + +function handleIndexPattern( + log: Logger, + indexRetriever: IndexPatternRetriever +): RequestHandler { + return async (context, req, res) => { + try { + return res.ok({ + body: { + indexPattern: await indexRetriever.getIndexPattern(context, req.params.datasetPath), + }, + }); + } catch (error) { + log.warn(error); + return res.notFound({ body: error }); + } + }; +} + +export function registerIndexPatternRoute(router: IRouter, endpointAppContext: EndpointAppContext) { + const log = endpointAppContext.logFactory.get('index_pattern'); + + router.get( + { + path: `${EndpointAppConstants.INDEX_PATTERN_ROUTE}/{datasetPath}`, + validate: { params: indexPatternGetParamsSchema }, + options: { authRequired: true }, + }, + handleIndexPattern(log, endpointAppContext.indexPatternRetriever) + ); +} diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts index 450469914bc50..883bb88204fd4 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -8,9 +8,9 @@ import { IRouter, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; +import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types'; import { EndpointAppContext } from '../../types'; -import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; interface HitSource { _source: HostMetadata; @@ -50,7 +50,14 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const queryParams = await kibanaRequestToMetadataListESQuery(req, endpointAppContext); + const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( + context + ); + const queryParams = await kibanaRequestToMetadataListESQuery( + req, + endpointAppContext, + index + ); const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', queryParams @@ -72,7 +79,11 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const doc = await getHostData(context, req.params.id); + const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( + context + ); + + const doc = await getHostData(context, req.params.id, index); if (doc) { return res.ok({ body: doc }); } @@ -86,9 +97,10 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp export async function getHostData( context: RequestHandlerContext, - id: string + id: string, + index: string ): Promise { - const query = getESQueryHostMetadataByID(id); + const query = getESQueryHostMetadataByID(id, index); const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', query diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts index 9bd251735cc04..9a7d3fb3188a6 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts @@ -11,24 +11,28 @@ import { RequestHandler, RequestHandlerContext, RouteConfig, + SavedObjectsClientContract, } from 'kibana/server'; import { elasticsearchServiceMock, httpServerMock, httpServiceMock, loggingServiceMock, + savedObjectsClientMock, } from '../../../../../../src/core/server/mocks'; import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types'; import { SearchResponse } from 'elasticsearch'; +import { registerEndpointRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import * as data from '../../test_data/all_metadata_data.json'; -import { registerEndpointRoutes } from './index'; +import { createMockMetadataIndexPatternRetriever } from '../../mocks'; describe('test endpoint route', () => { let routerMock: jest.Mocked; let mockResponse: jest.Mocked; let mockClusterClient: jest.Mocked; let mockScopedClient: jest.Mocked; + let mockSavedObjectClient: jest.Mocked; let routeHandler: RequestHandler; let routeConfig: RouteConfig; @@ -37,15 +41,38 @@ describe('test endpoint route', () => { IClusterClient >; mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockSavedObjectClient = savedObjectsClientMock.create(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); mockResponse = httpServerMock.createResponseFactory(); registerEndpointRoutes(routerMock, { + indexPatternRetriever: createMockMetadataIndexPatternRetriever(), logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); }); + function createRouteHandlerContext( + dataClient: jest.Mocked, + savedObjectsClient: jest.Mocked + ) { + return ({ + core: { + elasticsearch: { + dataClient, + }, + savedObjects: { + client: savedObjectsClient, + }, + }, + /** + * Using unknown here because the object defined is not a full `RequestHandlerContext`. We don't + * need all of the fields required to run the tests, but the `routeHandler` function requires a + * `RequestHandlerContext`. + */ + } as unknown) as RequestHandlerContext; + } + it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); @@ -58,13 +85,7 @@ describe('test endpoint route', () => { )!; await routeHandler( - ({ - core: { - elasticsearch: { - dataClient: mockScopedClient, - }, - }, - } as unknown) as RequestHandlerContext, + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); @@ -100,13 +121,7 @@ describe('test endpoint route', () => { )!; await routeHandler( - ({ - core: { - elasticsearch: { - dataClient: mockScopedClient, - }, - }, - } as unknown) as RequestHandlerContext, + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); @@ -147,13 +162,7 @@ describe('test endpoint route', () => { )!; await routeHandler( - ({ - core: { - elasticsearch: { - dataClient: mockScopedClient, - }, - }, - } as unknown) as RequestHandlerContext, + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); @@ -212,13 +221,7 @@ describe('test endpoint route', () => { )!; await routeHandler( - ({ - core: { - elasticsearch: { - dataClient: mockScopedClient, - }, - }, - } as unknown) as RequestHandlerContext, + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); @@ -243,13 +246,7 @@ describe('test endpoint route', () => { )!; await routeHandler( - ({ - core: { - elasticsearch: { - dataClient: mockScopedClient, - }, - }, - } as unknown) as RequestHandlerContext, + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); diff --git a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts index 2514d5aa85811..c8143fbdda1ea 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts @@ -6,7 +6,7 @@ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; import { EndpointConfigSchema } from '../../config'; import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; -import { EndpointAppConstants } from '../../../common/types'; +import { createMockMetadataIndexPatternRetriever, MetadataIndexPattern } from '../../mocks'; describe('query builder', () => { describe('MetadataListESQuery', () => { @@ -14,17 +14,22 @@ describe('query builder', () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {}, }); - const query = await kibanaRequestToMetadataListESQuery(mockRequest, { - logFactory: loggingServiceMock.create(), - config: () => Promise.resolve(EndpointConfigSchema.validate({})), - }); + const query = await kibanaRequestToMetadataListESQuery( + mockRequest, + { + indexPatternRetriever: createMockMetadataIndexPatternRetriever(), + logFactory: loggingServiceMock.create(), + config: () => Promise.resolve(EndpointConfigSchema.validate({})), + }, + MetadataIndexPattern + ); expect(query).toEqual({ body: { query: { match_all: {}, }, collapse: { - field: 'host.id.keyword', + field: 'host.id', inner_hits: { name: 'most_recent', size: 1, @@ -34,7 +39,7 @@ describe('query builder', () => { aggs: { total: { cardinality: { - field: 'host.id.keyword', + field: 'host.id', }, }, }, @@ -48,7 +53,7 @@ describe('query builder', () => { }, from: 0, size: 10, - index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + index: MetadataIndexPattern, } as Record); }); }); @@ -60,10 +65,15 @@ describe('query builder', () => { filter: 'not host.ip:10.140.73.246', }, }); - const query = await kibanaRequestToMetadataListESQuery(mockRequest, { - logFactory: loggingServiceMock.create(), - config: () => Promise.resolve(EndpointConfigSchema.validate({})), - }); + const query = await kibanaRequestToMetadataListESQuery( + mockRequest, + { + indexPatternRetriever: createMockMetadataIndexPatternRetriever(), + logFactory: loggingServiceMock.create(), + config: () => Promise.resolve(EndpointConfigSchema.validate({})), + }, + MetadataIndexPattern + ); expect(query).toEqual({ body: { query: { @@ -83,7 +93,7 @@ describe('query builder', () => { }, }, collapse: { - field: 'host.id.keyword', + field: 'host.id', inner_hits: { name: 'most_recent', size: 1, @@ -93,7 +103,7 @@ describe('query builder', () => { aggs: { total: { cardinality: { - field: 'host.id.keyword', + field: 'host.id', }, }, }, @@ -107,7 +117,7 @@ describe('query builder', () => { }, from: 0, size: 10, - index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + index: MetadataIndexPattern, } as Record); }); }); @@ -115,14 +125,15 @@ describe('query builder', () => { describe('MetadataGetQuery', () => { it('searches for the correct ID', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; - const query = getESQueryHostMetadataByID(mockID); + const query = getESQueryHostMetadataByID(mockID, MetadataIndexPattern); + expect(query).toEqual({ body: { - query: { match: { 'host.id.keyword': mockID } }, + query: { match: { 'host.id': mockID } }, sort: [{ 'event.created': { order: 'desc' } }], size: 1, }, - index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + index: MetadataIndexPattern, }); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts index bd07604fe9ad2..abcc293985f9f 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts @@ -6,18 +6,18 @@ import { KibanaRequest } from 'kibana/server'; import { esKuery } from '../../../../../../src/plugins/data/server'; import { EndpointAppContext } from '../../types'; -import { EndpointAppConstants } from '../../../common/types'; export const kibanaRequestToMetadataListESQuery = async ( request: KibanaRequest, - endpointAppContext: EndpointAppContext + endpointAppContext: EndpointAppContext, + index: string ): Promise> => { const pagingProperties = await getPagingProperties(request, endpointAppContext); return { body: { query: buildQueryBody(request), collapse: { - field: 'host.id.keyword', + field: 'host.id', inner_hits: { name: 'most_recent', size: 1, @@ -27,7 +27,7 @@ export const kibanaRequestToMetadataListESQuery = async ( aggs: { total: { cardinality: { - field: 'host.id.keyword', + field: 'host.id', }, }, }, @@ -41,7 +41,7 @@ export const kibanaRequestToMetadataListESQuery = async ( }, from: pagingProperties.pageIndex * pagingProperties.pageSize, size: pagingProperties.pageSize, - index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + index, }; }; @@ -74,12 +74,12 @@ function buildQueryBody(request: KibanaRequest): Record { return async (context, req, res) => { const { @@ -54,10 +56,11 @@ export function handleChildren( } = req; try { const pagination = getPaginationParams(limit, after); + const indexPattern = await indexRetriever.getEventIndexPattern(context); const client = context.core.elasticsearch.dataClient; - const childrenQuery = new ChildrenQuery(legacyEndpointID, pagination); - const lifecycleQuery = new LifecycleQuery(legacyEndpointID); + const childrenQuery = new ChildrenQuery(indexPattern, legacyEndpointID, pagination); + const lifecycleQuery = new LifecycleQuery(indexPattern, legacyEndpointID); // Retrieve the related child process events for a given process const { total, results: events, nextCursor } = await childrenQuery.search(client, id); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts index 9895344174014..6d155b79651a7 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts @@ -10,6 +10,7 @@ import { RequestHandler, Logger } from 'kibana/server'; import { extractParentEntityID } from './utils/normalize'; import { LifecycleQuery } from './queries/lifecycle'; import { ResolverEvent } from '../../../common/types'; +import { IndexPatternRetriever } from '../../index_pattern'; interface LifecycleQueryParams { ancestors: number; @@ -46,7 +47,8 @@ function getParentEntityID(results: ResolverEvent[]) { } export function handleLifecycle( - log: Logger + log: Logger, + indexRetriever: IndexPatternRetriever ): RequestHandler { return async (context, req, res) => { const { @@ -56,8 +58,8 @@ export function handleLifecycle( try { const ancestorLifecycles = []; const client = context.core.elasticsearch.dataClient; - - const lifecycleQuery = new LifecycleQuery(legacyEndpointID); + const indexPattern = await indexRetriever.getEventIndexPattern(context); + const lifecycleQuery = new LifecycleQuery(indexPattern, legacyEndpointID); const { results: processLifecycle } = await lifecycleQuery.search(client, id); let nextParentID = getParentEntityID(processLifecycle); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts index be83efc39ca4c..b049439207e50 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts @@ -11,6 +11,7 @@ import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public export abstract class ResolverQuery { constructor( + private readonly indexPattern: string, private readonly endpointID?: string, private readonly pagination?: PaginationParams ) {} @@ -26,7 +27,7 @@ export abstract class ResolverQuery { if (this.endpointID) { return this.legacyQuery(this.endpointID, ids, EndpointAppConstants.LEGACY_EVENT_INDEX_NAME); } - return this.query(ids, EndpointAppConstants.EVENT_INDEX_NAME); + return this.query(ids, this.indexPattern); } async search(client: IScopedClusterClient, ...ids: string[]) { diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts index 08a906e2884d6..e73053d53dee0 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts @@ -6,11 +6,17 @@ import { ChildrenQuery } from './children'; import { EndpointAppConstants } from '../../../../common/types'; +export const fakeEventIndexPattern = 'events-endpoint-*'; + describe('children events query', () => { it('generates the correct legacy queries', () => { const timestamp = new Date().getTime(); expect( - new ChildrenQuery('awesome-id', { size: 1, timestamp, eventID: 'foo' }).build('5') + new ChildrenQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id', { + size: 1, + timestamp, + eventID: 'foo', + }).build('5') ).toStrictEqual({ body: { query: { @@ -50,7 +56,11 @@ describe('children events query', () => { const timestamp = new Date().getTime(); expect( - new ChildrenQuery(undefined, { size: 1, timestamp, eventID: 'bar' }).build('baz') + new ChildrenQuery(fakeEventIndexPattern, undefined, { + size: 1, + timestamp, + eventID: 'bar', + }).build('baz') ).toStrictEqual({ body: { query: { @@ -88,7 +98,7 @@ describe('children events query', () => { size: 1, sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], }, - index: EndpointAppConstants.EVENT_INDEX_NAME, + index: fakeEventIndexPattern, }); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts index b1b47bfb9de7f..8a3955706b278 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts @@ -5,10 +5,13 @@ */ import { EndpointAppConstants } from '../../../../common/types'; import { LifecycleQuery } from './lifecycle'; +import { fakeEventIndexPattern } from './children.test'; describe('lifecycle query', () => { it('generates the correct legacy queries', () => { - expect(new LifecycleQuery('awesome-id').build('5')).toStrictEqual({ + expect( + new LifecycleQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id').build('5') + ).toStrictEqual({ body: { query: { bool: { @@ -32,7 +35,7 @@ describe('lifecycle query', () => { }); it('generates the correct non-legacy queries', () => { - expect(new LifecycleQuery().build('baz')).toStrictEqual({ + expect(new LifecycleQuery(fakeEventIndexPattern).build('baz')).toStrictEqual({ body: { query: { bool: { @@ -57,7 +60,7 @@ describe('lifecycle query', () => { }, sort: [{ '@timestamp': 'asc' }], }, - index: EndpointAppConstants.EVENT_INDEX_NAME, + index: fakeEventIndexPattern, }); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts index a91c87274b8dd..5caef935ce621 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts @@ -5,12 +5,17 @@ */ import { RelatedEventsQuery } from './related_events'; import { EndpointAppConstants } from '../../../../common/types'; +import { fakeEventIndexPattern } from './children.test'; describe('related events query', () => { it('generates the correct legacy queries', () => { const timestamp = new Date().getTime(); expect( - new RelatedEventsQuery('awesome-id', { size: 1, timestamp, eventID: 'foo' }).build('5') + new RelatedEventsQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id', { + size: 1, + timestamp, + eventID: 'foo', + }).build('5') ).toStrictEqual({ body: { query: { @@ -51,7 +56,11 @@ describe('related events query', () => { const timestamp = new Date().getTime(); expect( - new RelatedEventsQuery(undefined, { size: 1, timestamp, eventID: 'bar' }).build('baz') + new RelatedEventsQuery(fakeEventIndexPattern, undefined, { + size: 1, + timestamp, + eventID: 'bar', + }).build('baz') ).toStrictEqual({ body: { query: { @@ -90,7 +99,7 @@ describe('related events query', () => { size: 1, sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], }, - index: EndpointAppConstants.EVENT_INDEX_NAME, + index: fakeEventIndexPattern, }); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts index 804400522065c..46e205464f53c 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { RequestHandler, Logger } from 'kibana/server'; import { getPaginationParams } from './utils/pagination'; import { RelatedEventsQuery } from './queries/related_events'; +import { IndexPatternRetriever } from '../../index_pattern'; interface RelatedEventsQueryParams { after?: string; @@ -42,7 +43,8 @@ export const validateRelatedEvents = { }; export function handleRelatedEvents( - log: Logger + log: Logger, + indexRetriever: IndexPatternRetriever ): RequestHandler { return async (context, req, res) => { const { @@ -53,8 +55,9 @@ export function handleRelatedEvents( const pagination = getPaginationParams(limit, after); const client = context.core.elasticsearch.dataClient; + const indexPattern = await indexRetriever.getEventIndexPattern(context); // Retrieve the related non-process events for a given process - const relatedEventsQuery = new RelatedEventsQuery(legacyEndpointID, pagination); + const relatedEventsQuery = new RelatedEventsQuery(indexPattern, legacyEndpointID, pagination); const relatedEvents = await relatedEventsQuery.search(client, id); const { total, results: events, nextCursor } = relatedEvents; diff --git a/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json b/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json index 3c8486aa127ea..48952afb33f68 100644 --- a/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json +++ b/x-pack/plugins/endpoint/server/test_data/all_metadata_data.json @@ -1,115 +1,109 @@ { - "took" : 343, - "timed_out" : false, - "_shards" : { - "total" : 1, - "successful" : 1, - "skipped" : 0, - "failed" : 0 + "took": 343, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 }, - "hits" : { - "total" : { - "value" : 4, - "relation" : "eq" + "hits": { + "total": { + "value": 4, + "relation": "eq" }, - "max_score" : null, - "hits" : [ + "max_score": null, + "hits": [ { - "_index" : "endpoint-agent", - "_id" : "WqVo1G8BYQH1gtPUgYkC", - "_score" : null, - "_source" : { - "@timestamp" : 1579816615336, - "event" : { - "created" : "2020-01-23T21:56:55.336Z" + "_index": "metadata-endpoint-default-1", + "_id": "WqVo1G8BYQH1gtPUgYkC", + "_score": null, + "_source": { + "@timestamp": 1579816615336, + "event": { + "created": "2020-01-23T21:56:55.336Z" }, "elastic": { "agent": { "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2" } }, - "endpoint" : { - "policy" : { - "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + "endpoint": { + "policy": { + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" } }, - "agent" : { - "version" : "6.8.3", - "id" : "56a75650-3c8a-4e4f-ac17-6dd729c650e2", + "agent": { + "version": "6.8.3", + "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2", "name": "Elastic Endpoint" }, - "host" : { - "id" : "7141a48b-e19f-4ae3-89a0-6e7179a84265", - "hostname" : "larimer-0.example.com", - "ip" : "10.21.48.136", - "mac" : "77-be-30-f0-e8-d6", - "architecture" : "x86_64", - "os" : { - "name" : "windows 6.2", - "full" : "Windows Server 2012", - "version" : "6.2", - "variant" : "Windows Server" + "host": { + "id": "7141a48b-e19f-4ae3-89a0-6e7179a84265", + "hostname": "larimer-0.example.com", + "ip": "10.21.48.136", + "mac": "77-be-30-f0-e8-d6", + "architecture": "x86_64", + "os": { + "name": "windows 6.2", + "full": "Windows Server 2012", + "version": "6.2", + "variant": "Windows Server" } } }, - "fields" : { - "host.id.keyword" : [ - "7141a48b-e19f-4ae3-89a0-6e7179a84265" - ] + "fields": { + "host.id.keyword": ["7141a48b-e19f-4ae3-89a0-6e7179a84265"] }, - "sort" : [ - 1579816615336 - ], - "inner_hits" : { - "most_recent" : { - "hits" : { - "total" : { - "value" : 2, - "relation" : "eq" + "sort": [1579816615336], + "inner_hits": { + "most_recent": { + "hits": { + "total": { + "value": 2, + "relation": "eq" }, - "max_score" : null, - "hits" : [ + "max_score": null, + "hits": [ { - "_index" : "endpoint-agent", - "_id" : "WqVo1G8BYQH1gtPUgYkC", - "_score" : null, - "_source" : { - "@timestamp" : 1579816615336, - "event" : { - "created" : "2020-01-23T21:56:55.336Z" + "_index": "metadata-endpoint-default-1", + "_id": "WqVo1G8BYQH1gtPUgYkC", + "_score": null, + "_source": { + "@timestamp": 1579816615336, + "event": { + "created": "2020-01-23T21:56:55.336Z" }, "elastic": { "agent": { "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2" } }, - "endpoint" : { - "policy" : { - "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + "endpoint": { + "policy": { + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" } }, - "agent" : { - "version" : "6.8.3", - "id" : "56a75650-3c8a-4e4f-ac17-6dd729c650e2", + "agent": { + "version": "6.8.3", + "id": "56a75650-3c8a-4e4f-ac17-6dd729c650e2", "name": "Elastic Endpoint" }, - "host" : { - "id" : "7141a48b-e19f-4ae3-89a0-6e7179a84265", - "hostname" : "larimer-0.example.com", - "ip" : "10.21.48.136", - "mac" : "77-be-30-f0-e8-d6", - "architecture" : "x86_64", - "os" : { - "name" : "windows 6.2", - "full" : "Windows Server 2012", - "version" : "6.2", - "variant" : "Windows Server" + "host": { + "id": "7141a48b-e19f-4ae3-89a0-6e7179a84265", + "hostname": "larimer-0.example.com", + "ip": "10.21.48.136", + "mac": "77-be-30-f0-e8-d6", + "architecture": "x86_64", + "os": { + "name": "windows 6.2", + "full": "Windows Server 2012", + "version": "6.2", + "variant": "Windows Server" } } }, - "sort" : [ - 1579816615336 - ] + "sort": [1579816615336] } ] } @@ -117,101 +111,95 @@ } }, { - "_index" : "endpoint-agent", - "_id" : "W6Vo1G8BYQH1gtPUgYkC", - "_score" : null, - "_source" : { - "@timestamp" : 1579816615336, - "event" : { - "created" : "2020-01-23T21:56:55.336Z" + "_index": "metadata-endpoint-default-1", + "_id": "W6Vo1G8BYQH1gtPUgYkC", + "_score": null, + "_source": { + "@timestamp": 1579816615336, + "event": { + "created": "2020-01-23T21:56:55.336Z" }, "elastic": { "agent": { "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312" } }, - "endpoint" : { - "policy" : { - "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + "endpoint": { + "policy": { + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" } }, - "agent" : { - "version" : "6.4.3", - "id" : "c2d84d8f-d355-40de-8b54-5d318d4d1312", + "agent": { + "version": "6.4.3", + "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312", "name": "Elastic Endpoint" }, - "host" : { - "id" : "f35ec6c1-6562-45b1-818f-2f14c0854adf", - "hostname" : "hildebrandt-6.example.com", - "ip" : "10.53.92.84", - "mac" : "af-f1-8f-51-25-2a", - "architecture" : "x86_64", - "os" : { - "name" : "windows 10.0", - "full" : "Windows 10", - "version" : "10.0", - "variant" : "Windows Pro" + "host": { + "id": "f35ec6c1-6562-45b1-818f-2f14c0854adf", + "hostname": "hildebrandt-6.example.com", + "ip": "10.53.92.84", + "mac": "af-f1-8f-51-25-2a", + "architecture": "x86_64", + "os": { + "name": "windows 10.0", + "full": "Windows 10", + "version": "10.0", + "variant": "Windows Pro" } } }, - "fields" : { - "host.id.keyword" : [ - "f35ec6c1-6562-45b1-818f-2f14c0854adf" - ] + "fields": { + "host.id.keyword": ["f35ec6c1-6562-45b1-818f-2f14c0854adf"] }, - "sort" : [ - 1579816615336 - ], - "inner_hits" : { - "most_recent" : { - "hits" : { - "total" : { - "value" : 2, - "relation" : "eq" + "sort": [1579816615336], + "inner_hits": { + "most_recent": { + "hits": { + "total": { + "value": 2, + "relation": "eq" }, - "max_score" : null, - "hits" : [ + "max_score": null, + "hits": [ { - "_index" : "endpoint-agent", - "_id" : "W6Vo1G8BYQH1gtPUgYkC", - "_score" : null, - "_source" : { - "@timestamp" : 1579816615336, - "event" : { - "created" : "2020-01-23T21:56:55.336Z" + "_index": "metadata-endpoint-default-1", + "_id": "W6Vo1G8BYQH1gtPUgYkC", + "_score": null, + "_source": { + "@timestamp": 1579816615336, + "event": { + "created": "2020-01-23T21:56:55.336Z" }, "elastic": { "agent": { "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312" } }, - "endpoint" : { - "policy" : { - "id" : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + "endpoint": { + "policy": { + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" } }, - "agent" : { - "version" : "6.4.3", - "id" : "c2d84d8f-d355-40de-8b54-5d318d4d1312", + "agent": { + "version": "6.4.3", + "id": "c2d84d8f-d355-40de-8b54-5d318d4d1312", "name": "Elastic Endpoint" }, - "host" : { - "id" : "f35ec6c1-6562-45b1-818f-2f14c0854adf", - "hostname" : "hildebrandt-6.example.com", - "ip" : "10.53.92.84", - "mac" : "af-f1-8f-51-25-2a", - "architecture" : "x86_64", - "os" : { - "name" : "windows 10.0", - "full" : "Windows 10", - "version" : "10.0", - "variant" : "Windows Pro" + "host": { + "id": "f35ec6c1-6562-45b1-818f-2f14c0854adf", + "hostname": "hildebrandt-6.example.com", + "ip": "10.53.92.84", + "mac": "af-f1-8f-51-25-2a", + "architecture": "x86_64", + "os": { + "name": "windows 10.0", + "full": "Windows 10", + "version": "10.0", + "variant": "Windows Pro" } } }, - "sort" : [ - 1579816615336 - ] + "sort": [1579816615336] } ] } @@ -220,9 +208,9 @@ } ] }, - "aggregations" : { - "total" : { - "value" : 2 + "aggregations": { + "total": { + "value": 2 } } } diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts index 6dc128bd3d61e..46a23060339f4 100644 --- a/x-pack/plugins/endpoint/server/types.ts +++ b/x-pack/plugins/endpoint/server/types.ts @@ -5,11 +5,13 @@ */ import { LoggerFactory } from 'kibana/server'; import { EndpointConfigType } from './config'; +import { IndexPatternRetriever } from './index_pattern'; /** * The context for Endpoint apps. */ export interface EndpointAppContext { + indexPatternRetriever: IndexPatternRetriever; logFactory: LoggerFactory; config(): Promise; } diff --git a/x-pack/plugins/features/kibana.json b/x-pack/plugins/features/kibana.json index 325e5a0407493..6e51f3b650710 100644 --- a/x-pack/plugins/features/kibana.json +++ b/x-pack/plugins/features/kibana.json @@ -2,7 +2,7 @@ "id": "features", "version": "8.0.0", "kibanaVersion": "kibana", - "optionalPlugins": ["timelion"], + "optionalPlugins": ["visTypeTimelion"], "configPath": ["xpack", "features"], "server": true, "ui": true diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index cebf67243fb28..83cc9e1eb7cc8 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -13,7 +13,7 @@ import { import { Capabilities as UICapabilities } from '../../../../src/core/server'; import { deepFreeze } from '../../../../src/core/utils'; import { XPackInfo } from '../../../legacy/plugins/xpack_main/server/lib/xpack_info'; -import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/timelion/server'; +import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/vis_type_timelion/server'; import { FeatureRegistry } from './feature_registry'; import { Feature, FeatureConfig } from '../common/feature'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; @@ -65,7 +65,7 @@ export class Plugin { public async setup( core: CoreSetup, - { timelion }: { timelion?: TimelionSetupContract } + { visTypeTimelion }: { visTypeTimelion?: TimelionSetupContract } ): Promise> { defineRoutes({ router: core.http.createRouter(), @@ -84,7 +84,7 @@ export class Plugin { // Register OSS features. for (const feature of buildOSSFeatures({ savedObjectTypes: this.legacyAPI.savedObjectTypes, - includeTimelion: timelion !== undefined && timelion.uiEnabled, + includeTimelion: visTypeTimelion !== undefined && visTypeTimelion.uiEnabled, })) { this.featureRegistry.register(feature); } diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts index 73c399878b17b..35dcc4cf42b37 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts @@ -135,6 +135,42 @@ describe('populateUICapabilities', () => { }); }); + it(`supports capabilities from reserved privileges`, () => { + expect( + uiCapabilitiesForFeatures([ + new Feature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: null, + reserved: { + description: '', + privileges: [ + { + id: 'rp_1', + privilege: createFeaturePrivilege(['capability1', 'capability2']), + }, + { + id: 'rp_2', + privilege: createFeaturePrivilege(['capability3', 'capability4', 'capability5']), + }, + ], + }, + }), + ]) + ).toEqual({ + catalogue: {}, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + capability5: true, + }, + }); + }); + it(`supports merging features with sub privileges`, () => { expect( uiCapabilitiesForFeatures([ diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.ts index d3d3230822749..e6ff3ad4383d2 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -45,6 +45,9 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { ...feature.subFeatures.map(sf => sf.privilegeGroups.map(pg => pg.privileges)).flat(2) ); } + if (feature.reserved?.privileges) { + featurePrivileges.push(...feature.reserved.privileges.map(rp => rp.privilege)); + } featurePrivileges.forEach(privilege => { UIFeatureCapabilities[feature.id] = { diff --git a/x-pack/plugins/file_upload/server/telemetry/mappings.ts b/x-pack/plugins/file_upload/server/telemetry/mappings.ts index ca935fea3449a..97a5ed9eeef82 100644 --- a/x-pack/plugins/file_upload/server/telemetry/mappings.ts +++ b/x-pack/plugins/file_upload/server/telemetry/mappings.ts @@ -10,7 +10,7 @@ import { TELEMETRY_DOC_ID } from './telemetry'; export const fileUploadTelemetryMappingsType: SavedObjectsType = { name: TELEMETRY_DOC_ID, hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { filesUploadedTotalCount: { diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index b8796ad7a358e..a15465a0cde66 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -10,7 +10,7 @@ "home", "data", "dataEnhanced", - "metrics", + "visTypeTimeseries", "alerting", "triggers_actions_ui" ], diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 8ddd3935bcc33..038fd457fb6c7 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -20,7 +20,7 @@ export interface InfraServerPluginDeps { home: HomeServerPluginSetup; spaces: SpacesPluginSetup; usageCollection: UsageCollectionSetup; - metrics: VisTypeTimeseriesSetup; + visTypeTimeseries: VisTypeTimeseriesSetup; features: FeaturesPluginSetup; apm: APMPluginContract; alerting: AlertingPluginContract; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index b73acd6703054..eda1fbfa5f4ce 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -245,7 +245,7 @@ export class KibanaFramework { timerange: { min: number; max: number }, filters: any[] ): Promise { - const { getVisData } = this.plugins.metrics; + const { getVisData } = this.plugins.visTypeTimeseries; if (typeof getVisData !== 'function') { throw new Error('TSVB is not available'); } diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 064341c68a97a..53ad0310ea613 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -218,6 +218,7 @@ export type PackageInfo = Installable< export interface Installation extends SavedObjectAttributes { installed: AssetReference[]; + es_index_patterns: Record; name: string; version: string; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx index c1cdde730837f..56b109a9bc062 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx @@ -25,7 +25,15 @@ import { import { ShellEnrollmentInstructions } from '../../../../../components/enrollment_instructions'; import { Loading } from '../../../../../components'; -const CONFIG_KEYS_ORDER = ['id', 'revision', 'outputs', 'datasources']; +const CONFIG_KEYS_ORDER = [ + 'id', + 'revision', + 'outputs', + 'datasources', + 'enabled', + 'package', + 'input', +]; export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => { const core = useCore(); @@ -47,7 +55,17 @@ export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => { {dump(fullConfigRequest.data.item, { sortKeys: (keyA: string, keyB: string) => { - return CONFIG_KEYS_ORDER.indexOf(keyA) - CONFIG_KEYS_ORDER.indexOf(keyB); + const indexA = CONFIG_KEYS_ORDER.indexOf(keyA); + const indexB = CONFIG_KEYS_ORDER.indexOf(keyB); + if (indexA >= 0 && indexB < 0) { + return -1; + } + + if (indexA < 0 && indexB >= 0) { + return 1; + } + + return indexA - indexB; }, })} diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index df7c3d7cf0fbf..7859c44ccfd89 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -7,6 +7,9 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; +export { ESIndexPatternService } from './services'; +export { IngestManagerSetupContract } from './plugin'; + export const config = { exposeToBrowser: { epm: true, diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 45c847fe1f68a..4dd070a7414f0 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -11,7 +11,9 @@ import { Plugin, PluginInitializerContext, SavedObjectsServiceStart, -} from 'src/core/server'; + RecursiveReadonly, +} from 'kibana/server'; +import { deepFreeze } from '../../../../src/core/utils'; import { LicensingPluginSetup } from '../../licensing/server'; import { EncryptedSavedObjectsPluginStart } from '../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -38,7 +40,18 @@ import { } from './routes'; import { IngestManagerConfigType } from '../common'; -import { appContextService } from './services'; +import { + appContextService, + ESIndexPatternService, + ESIndexPatternSavedObjectService, +} from './services'; + +/** + * Describes public IngestManager plugin contract returned at the `setup` stage. + */ +export interface IngestManagerSetupContract { + esIndexPatternService: ESIndexPatternService; +} export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; @@ -63,7 +76,7 @@ const allSavedObjectTypes = [ ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, ]; -export class IngestManagerPlugin implements Plugin { +export class IngestManagerPlugin implements Plugin { private config$: Observable; private security: SecurityPluginSetup | undefined; @@ -71,7 +84,10 @@ export class IngestManagerPlugin implements Plugin { this.config$ = this.initializerContext.config.create(); } - public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { + public async setup( + core: CoreSetup, + deps: IngestManagerSetupDeps + ): Promise> { if (deps.security) { this.security = deps.security; } @@ -130,6 +146,9 @@ export class IngestManagerPlugin implements Plugin { basePath: core.http.basePath, }); } + return deepFreeze({ + esIndexPatternService: new ESIndexPatternSavedObjectService(), + }); } public async start( diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 1c36fda36847d..dc0b4695603e4 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -150,6 +150,10 @@ export const savedObjectMappings = { name: { type: 'keyword' }, version: { type: 'keyword' }, internal: { type: 'boolean' }, + es_index_patterns: { + dynamic: false, + type: 'object', + }, installed: { type: 'nested', properties: { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 0e239c24dd9cf..166983fbccc35 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -47,12 +47,12 @@ exports[`tests loading base.yml: base.yml 1`] = ` "user": { "properties": { "auid": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "euid": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } }, @@ -73,12 +73,12 @@ exports[`tests loading base.yml: base.yml 1`] = ` "nested": { "properties": { "bar": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "baz": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } }, @@ -142,8 +142,8 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` "coredns": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "query": { "properties": { @@ -151,28 +151,28 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` "type": "long" }, "class": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "name": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "type": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } }, "response": { "properties": { "code": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "flags": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "size": { "type": "long" @@ -509,12 +509,12 @@ exports[`tests loading system.yml: system.yml 1`] = ` "diskio": { "properties": { "name": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "serial_number": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "read": { "properties": { @@ -643,16 +643,16 @@ exports[`tests loading system.yml: system.yml 1`] = ` "type": "long" }, "device_name": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "type": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "mount_point": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "files": { "type": "long" @@ -867,8 +867,8 @@ exports[`tests loading system.yml: system.yml 1`] = ` "network": { "properties": { "name": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "out": { "properties": { @@ -946,12 +946,12 @@ exports[`tests loading system.yml: system.yml 1`] = ` "process": { "properties": { "state": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "cmdline": { - "type": "keyword", - "ignore_above": 2048 + "ignore_above": 2048, + "type": "keyword" }, "env": { "type": "object" @@ -1040,22 +1040,22 @@ exports[`tests loading system.yml: system.yml 1`] = ` "cgroup": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "cpu": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "cfs": { "properties": { @@ -1118,12 +1118,12 @@ exports[`tests loading system.yml: system.yml 1`] = ` "cpuacct": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "total": { "properties": { @@ -1158,12 +1158,12 @@ exports[`tests loading system.yml: system.yml 1`] = ` "memory": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "mem": { "properties": { @@ -1382,12 +1382,12 @@ exports[`tests loading system.yml: system.yml 1`] = ` "blkio": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "total": { "properties": { @@ -1436,20 +1436,20 @@ exports[`tests loading system.yml: system.yml 1`] = ` "raid": { "properties": { "name": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "status": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "level": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "sync_action": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "disks": { "properties": { @@ -1507,24 +1507,24 @@ exports[`tests loading system.yml: system.yml 1`] = ` "type": "long" }, "host": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "etld_plus_one": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "host_error": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } }, "process": { "properties": { "cmdline": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } }, @@ -1622,42 +1622,42 @@ exports[`tests loading system.yml: system.yml 1`] = ` "users": { "properties": { "id": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "seat": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "path": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "type": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "service": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "remote": { "type": "boolean" }, "state": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "scope": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" }, "leader": { "type": "long" }, "remote_host": { - "type": "keyword", - "ignore_above": 1024 + "ignore_above": 1024, + "type": "keyword" } } } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts index f4e13748641ed..1a73c9581a2de 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts @@ -63,3 +63,199 @@ test('tests loading system.yml', () => { expect(template).toMatchSnapshot(path.basename(ymlPath)); }); + +test('tests processing text field with multi fields', () => { + const textWithMultiFieldsLiteralYml = ` +- name: textWithMultiFields + type: text + multi_fields: + - name: raw + type: keyword + - name: indexed + type: text +`; + const textWithMultiFieldsMapping = { + properties: { + textWithMultiFields: { + type: 'text', + fields: { + raw: { + ignore_above: 1024, + type: 'keyword', + }, + indexed: { + type: 'text', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(textWithMultiFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(textWithMultiFieldsMapping)); +}); + +test('tests processing keyword field with multi fields', () => { + const keywordWithMultiFieldsLiteralYml = ` +- name: keywordWithMultiFields + type: keyword + multi_fields: + - name: raw + type: keyword + - name: indexed + type: text +`; + + const keywordWithMultiFieldsMapping = { + properties: { + keywordWithMultiFields: { + ignore_above: 1024, + type: 'keyword', + fields: { + raw: { + ignore_above: 1024, + type: 'keyword', + }, + indexed: { + type: 'text', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(keywordWithMultiFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(keywordWithMultiFieldsMapping)); +}); + +test('tests processing keyword field with multi fields with analyzed text field', () => { + const keywordWithAnalyzedMultiFieldsLiteralYml = ` + - name: keywordWithAnalyzedMultiField + type: keyword + multi_fields: + - name: analyzed + type: text + analyzer: autocomplete + search_analyzer: standard + `; + + const keywordWithAnalyzedMultiFieldsMapping = { + properties: { + keywordWithAnalyzedMultiField: { + ignore_above: 1024, + type: 'keyword', + fields: { + analyzed: { + analyzer: 'autocomplete', + search_analyzer: 'standard', + type: 'text', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(keywordWithAnalyzedMultiFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(keywordWithAnalyzedMultiFieldsMapping)); +}); + +test('tests processing object field with no other attributes', () => { + const objectFieldLiteralYml = ` +- name: objectField + type: object +`; + const objectFieldMapping = { + properties: { + objectField: { + type: 'object', + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldMapping)); +}); + +test('tests processing object field with enabled set to false', () => { + const objectFieldEnabledFalseLiteralYml = ` +- name: objectField + type: object + enabled: false +`; + const objectFieldEnabledFalseMapping = { + properties: { + objectField: { + type: 'object', + enabled: false, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldEnabledFalseLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldEnabledFalseMapping)); +}); + +test('tests processing object field with dynamic set to false', () => { + const objectFieldDynamicFalseLiteralYml = ` +- name: objectField + type: object + dynamic: false +`; + const objectFieldDynamicFalseMapping = { + properties: { + objectField: { + type: 'object', + dynamic: false, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldDynamicFalseLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicFalseMapping)); +}); + +test('tests processing object field with dynamic set to true', () => { + const objectFieldDynamicTrueLiteralYml = ` +- name: objectField + type: object + dynamic: true +`; + const objectFieldDynamicTrueMapping = { + properties: { + objectField: { + type: 'object', + dynamic: true, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldDynamicTrueLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicTrueMapping)); +}); + +test('tests processing object field with dynamic set to strict', () => { + const objectFieldDynamicStrictLiteralYml = ` +- name: objectField + type: object + dynamic: strict +`; + const objectFieldDynamicStrictMapping = { + properties: { + objectField: { + type: 'object', + dynamic: 'strict', + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldDynamicStrictLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicStrictMapping)); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 71c9acc6c10da..22a61d2bdfb7c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Field } from '../../fields/field'; +import { Field, Fields } from '../../fields/field'; import { Dataset, IndexTemplate } from '../../../../types'; import { getDatasetAssetBaseName } from '../index'; @@ -15,6 +15,14 @@ interface Mappings { properties: any; } +interface Mapping { + [key: string]: any; +} + +interface MultiFields { + [key: string]: object; +} + const DEFAULT_SCALING_FACTOR = 1000; const DEFAULT_IGNORE_ABOVE = 1024; @@ -67,26 +75,27 @@ export function generateMappings(fields: Field[]): Mappings { fieldProps.scaling_factor = field.scaling_factor || DEFAULT_SCALING_FACTOR; break; case 'text': - fieldProps.type = 'text'; - if (field.analyzer) { - fieldProps.analyzer = field.analyzer; - } - if (field.search_analyzer) { - fieldProps.search_analyzer = field.search_analyzer; + const textMapping = generateTextMapping(field); + fieldProps = { ...fieldProps, ...textMapping, type: 'text' }; + if (field.multi_fields) { + fieldProps.fields = generateMultiFields(field.multi_fields); } break; case 'keyword': - fieldProps.type = 'keyword'; - if (field.ignore_above) { - fieldProps.ignore_above = field.ignore_above; - } else { - fieldProps.ignore_above = DEFAULT_IGNORE_ABOVE; + const keywordMapping = generateKeywordMapping(field); + fieldProps = { ...fieldProps, ...keywordMapping, type: 'keyword' }; + if (field.multi_fields) { + fieldProps.fields = generateMultiFields(field.multi_fields); } break; - // TODO move handling of multi_fields here? case 'object': - // TODO improve fieldProps.type = 'object'; + if (field.hasOwnProperty('enabled')) { + fieldProps.enabled = field.enabled; + } + if (field.hasOwnProperty('dynamic')) { + fieldProps.dynamic = field.dynamic; + } break; case 'array': // this assumes array fields were validated in an earlier step @@ -113,6 +122,45 @@ export function generateMappings(fields: Field[]): Mappings { return { properties: props }; } +function generateMultiFields(fields: Fields): MultiFields { + const multiFields: MultiFields = {}; + if (fields) { + fields.forEach((f: Field) => { + const type = f.type; + switch (type) { + case 'text': + multiFields[f.name] = { ...generateTextMapping(f), type: f.type }; + break; + case 'keyword': + multiFields[f.name] = { ...generateKeywordMapping(f), type: f.type }; + break; + } + }); + } + return multiFields; +} + +function generateKeywordMapping(field: Field): Mapping { + const mapping: Mapping = { + ignore_above: DEFAULT_IGNORE_ABOVE, + }; + if (field.ignore_above) { + mapping.ignore_above = field.ignore_above; + } + return mapping; +} + +function generateTextMapping(field: Field): Mapping { + const mapping: Mapping = {}; + if (field.analyzer) { + mapping.analyzer = field.analyzer; + } + if (field.search_analyzer) { + mapping.search_analyzer = field.search_analyzer; + } + return mapping; +} + function getDefaultProperties(field: Field): Properties { const properties: Properties = {}; @@ -136,6 +184,22 @@ export function generateTemplateName(dataset: Dataset): string { return getDatasetAssetBaseName(dataset); } +/** + * Returns a map of the dataset path fields to elasticsearch index pattern. + * @param datasets an array of Dataset objects + */ +export function generateESIndexPatterns(datasets: Dataset[] | undefined): Record { + if (!datasets) { + return {}; + } + + const patterns: Record = {}; + for (const dataset of datasets) { + patterns[dataset.path] = generateTemplateName(dataset) + '-*'; + } + return patterns; +} + function getBaseTemplate(type: string, templateName: string, mappings: Mappings): IndexTemplate { return { // We need to decide which order we use for the templates diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts index 810896bb50389..9c9843e0454ab 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts @@ -27,6 +27,7 @@ export interface Field { ignore_above?: number; object_type?: string; scaling_factor?: number; + dynamic?: 'strict' | boolean; // Kibana specific analyzed?: boolean; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index e250b4f176819..0a7642752b3e9 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -18,6 +18,7 @@ import * as Registry from '../registry'; import { getObject } from './get_objects'; import { getInstallation } from './index'; import { installTemplates } from '../elasticsearch/template/install'; +import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines } from '../elasticsearch/ingest_pipeline/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; @@ -117,17 +118,18 @@ export async function installPackage(options: { installTemplatePromises, ]); - const toSave = res.flat(); + const toSaveAssetRefs: AssetReference[] = res.flat(); + const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); // Save those references in the package manager's state saved object - await saveInstallationReferences({ + return await saveInstallationReferences({ savedObjectsClient, pkgkey, pkgName, pkgVersion, internal, - toSave, + toSaveAssetRefs, + toSaveESIndexPatterns, }); - return toSave; } // TODO: make it an exhaustive list @@ -156,25 +158,44 @@ export async function saveInstallationReferences(options: { pkgName: string; pkgVersion: string; internal: boolean; - toSave: AssetReference[]; + toSaveAssetRefs: AssetReference[]; + toSaveESIndexPatterns: Record; }) { - const { savedObjectsClient, pkgName, pkgVersion, internal, toSave } = options; + const { + savedObjectsClient, + pkgName, + pkgVersion, + internal, + toSaveAssetRefs, + toSaveESIndexPatterns, + } = options; const installation = await getInstallation({ savedObjectsClient, pkgName }); - const savedRefs = installation?.installed || []; + const savedAssetRefs = installation?.installed || []; + const toInstallESIndexPatterns = Object.assign( + installation?.es_index_patterns || {}, + toSaveESIndexPatterns + ); + const mergeRefsReducer = (current: AssetReference[], pending: AssetReference) => { const hasRef = current.find(c => c.id === pending.id && c.type === pending.type); if (!hasRef) current.push(pending); return current; }; - const toInstall = toSave.reduce(mergeRefsReducer, savedRefs); + const toInstallAssetsRefs = toSaveAssetRefs.reduce(mergeRefsReducer, savedAssetRefs); await savedObjectsClient.create( PACKAGES_SAVED_OBJECT_TYPE, - { installed: toInstall, name: pkgName, version: pkgVersion, internal }, + { + installed: toInstallAssetsRefs, + es_index_patterns: toInstallESIndexPatterns, + name: pkgName, + version: pkgVersion, + internal, + }, { id: pkgName, overwrite: true } ); - return toInstall; + return toInstallAssetsRefs; } async function installKibanaSavedObjects({ diff --git a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts new file mode 100644 index 0000000000000..167e22873979c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.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 { SavedObjectsClientContract } from 'kibana/server'; +import { getInstallation } from './epm/packages/get'; + +export interface ESIndexPatternService { + getESIndexPattern( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + datasetPath: string + ): Promise; +} + +export class ESIndexPatternSavedObjectService implements ESIndexPatternService { + public async getESIndexPattern( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + datasetPath: string + ): Promise { + const installation = await getInstallation({ savedObjectsClient, pkgName }); + return installation?.es_index_patterns[datasetPath]; + } +} diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index dd0c898afa425..d64f1b0c2b6fb 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ export { appContextService } from './app_context'; +export { ESIndexPatternService, ESIndexPatternSavedObjectService } from './es_index_pattern'; // Saved object services export { datasourceService } from './datasource'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 8b03a879da431..b3bd08d3bbfbe 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -140,12 +140,16 @@ export function onDrop( operationsForNewField && operationsForNewField.includes(selectedColumn.operationType); + if (!operationsForNewField || operationsForNewField.length === 0) { + return false; + } + // If only the field has changed use the onFieldChange method on the operation to get the // new column, otherwise use the regular buildColumn to get a new column. const newColumn = hasFieldChanged ? changeField(selectedColumn, currentIndexPattern, droppedItem.field) : buildColumn({ - op: operationsForNewField ? operationsForNewField[0] : undefined, + op: operationsForNewField[0], columns: props.state.layers[props.layerId].columns, indexPattern: currentIndexPattern, layerId, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index dbdbe4e3f9442..e4f3677d0fe88 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -257,12 +257,54 @@ describe('IndexPattern Data Source', () => { const state = stateFromPersistedState(queryPersistedState); expect(indexPatternDatasource.toExpression(state, 'first')).toMatchInlineSnapshot(` - "esaggs - index=\\"1\\" - metricsAtAllLevels=false - partialRows=false - includeFormatHints=true - aggConfigs={lens_auto_date aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]'} | lens_rename_columns idMap='{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}' " + Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "aggConfigs": Array [ + "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]", + ], + }, + "function": "lens_auto_date", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "includeFormatHints": Array [ + true, + ], + "index": Array [ + "1", + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + false, + ], + }, + "function": "esaggs", + "type": "function", + }, + Object { + "arguments": Object { + "idMap": Array [ + "{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}", + ], + }, + "function": "lens_rename_columns", + "type": "function", + }, + ], + "type": "expression", + } `); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index d339171a5ae1f..2b3e976a77ea7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -14,6 +14,7 @@ import { getOperationTypesForField, operationDefinitionMap, IndexPatternColumn, + OperationType, } from './operations'; import { operationDefinitions } from './operations/definitions'; import { hasField } from './utils'; @@ -141,7 +142,13 @@ function getExistingLayerSuggestionsForField( suggestions.push( buildSuggestion({ state, - updatedLayer: addFieldAsBucketOperation(layer, layerId, indexPattern, field), + updatedLayer: addFieldAsBucketOperation( + layer, + layerId, + indexPattern, + field, + usableAsBucketOperation + ), layerId, changeType: 'extended', }) @@ -176,26 +183,7 @@ function addFieldAsMetricOperation( indexPattern: IndexPattern, field: IndexPatternField ): IndexPatternLayer | undefined { - const operations = getOperationTypesForField(field); - const operationsAlreadyAppliedToThisField = Object.values(layer.columns) - .filter(column => hasField(column) && column.sourceField === field.name) - .map(column => column.operationType); - const operationCandidate = operations.find( - operation => !operationsAlreadyAppliedToThisField.includes(operation) - ); - - if (!operationCandidate) { - return; - } - - const newColumn = buildColumn({ - op: operationCandidate, - columns: layer.columns, - layerId, - indexPattern, - suggestedPriority: undefined, - field, - }); + const newColumn = getMetricColumn(indexPattern, layerId, field); const addedColumnId = generateId(); const [, metrics] = separateBucketColumns(layer); @@ -226,11 +214,11 @@ function addFieldAsBucketOperation( layer: IndexPatternLayer, layerId: string, indexPattern: IndexPattern, - field: IndexPatternField + field: IndexPatternField, + operation: OperationType ): IndexPatternLayer { - const applicableBucketOperation = getBucketOperation(field); const newColumn = buildColumn({ - op: applicableBucketOperation, + op: operation, columns: layer.columns, layerId, indexPattern, @@ -252,7 +240,7 @@ function addFieldAsBucketOperation( let updatedColumnOrder: string[] = []; if (oldDateHistogramId) { - if (applicableBucketOperation === 'terms') { + if (operation === 'terms') { // Insert the new terms bucket above the first date histogram updatedColumnOrder = [ ...buckets.slice(0, oldDateHistogramIndex), @@ -260,7 +248,7 @@ function addFieldAsBucketOperation( ...buckets.slice(oldDateHistogramIndex, buckets.length), ...metrics, ]; - } else if (applicableBucketOperation === 'date_histogram') { + } else if (operation === 'date_histogram') { // Replace date histogram with new date histogram delete updatedColumns[oldDateHistogramId]; updatedColumnOrder = layer.columnOrder.map(columnId => @@ -287,8 +275,9 @@ function getEmptyLayerSuggestionsForField( ): IndexPatternSugestion[] { const indexPattern = state.indexPatterns[indexPatternId]; let newLayer: IndexPatternLayer | undefined; - if (getBucketOperation(field)) { - newLayer = createNewLayerWithBucketAggregation(layerId, indexPattern, field); + const bucketOperation = getBucketOperation(field); + if (bucketOperation) { + newLayer = createNewLayerWithBucketAggregation(layerId, indexPattern, field, bucketOperation); } else if (indexPattern.timeFieldName && getOperationTypesForField(field).length > 0) { newLayer = createNewLayerWithMetricAggregation(layerId, indexPattern, field); } @@ -312,7 +301,8 @@ function getEmptyLayerSuggestionsForField( function createNewLayerWithBucketAggregation( layerId: string, indexPattern: IndexPattern, - field: IndexPatternField + field: IndexPatternField, + operation: OperationType ): IndexPatternLayer { const countColumn = buildColumn({ op: 'count', @@ -329,7 +319,7 @@ function createNewLayerWithBucketAggregation( // let column know about count column const column = buildColumn({ layerId, - op: getBucketOperation(field), + op: operation, indexPattern, columns: { [col2]: countColumn, @@ -355,15 +345,7 @@ function createNewLayerWithMetricAggregation( ): IndexPatternLayer { const dateField = indexPattern.fields.find(f => f.name === indexPattern.timeFieldName)!; - const operations = getOperationTypesForField(field); - const column = buildColumn({ - op: operations[0], - columns: {}, - suggestedPriority: undefined, - field, - indexPattern, - layerId, - }); + const column = getMetricColumn(indexPattern, layerId, field); const dateColumn = buildColumn({ op: 'date_histogram', @@ -500,12 +482,7 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId }); } -function createMetricSuggestion( - indexPattern: IndexPattern, - layerId: string, - state: IndexPatternPrivateState, - field: IndexPatternField -) { +function getMetricColumn(indexPattern: IndexPattern, layerId: string, field: IndexPatternField) { const operationDefinitionsMap = _.indexBy(operationDefinitions, 'type'); const [column] = getOperationTypesForField(field) .map(type => @@ -518,6 +495,16 @@ function createMetricSuggestion( }) ) .filter(op => (op.dataType === 'number' || op.dataType === 'document') && !op.isBucketed); + return column; +} + +function createMetricSuggestion( + indexPattern: IndexPattern, + layerId: string, + state: IndexPatternPrivateState, + field: IndexPatternField +) { + const column = getMetricColumn(indexPattern, layerId, field); if (!column) { return; @@ -572,21 +559,26 @@ function createAlternativeMetricSuggestions( return; } const field = indexPattern.fields.find(({ name }) => column.sourceField === name)!; - const alternativeMetricOperations = getOperationTypesForField(field).filter( - operationType => operationType !== column.operationType - ); + const alternativeMetricOperations = getOperationTypesForField(field) + .map(op => + buildColumn({ + op, + columns: layer.columns, + indexPattern, + layerId, + field, + suggestedPriority: undefined, + }) + ) + .filter( + fullOperation => + fullOperation.operationType !== column.operationType && !fullOperation.isBucketed + ); if (alternativeMetricOperations.length === 0) { return; } const newId = generateId(); - const newColumn = buildColumn({ - op: alternativeMetricOperations[0], - columns: layer.columns, - indexPattern, - layerId, - field, - suggestedPriority: undefined, - }); + const newColumn = alternativeMetricOperations[0]; const updatedLayer = { indexPatternId: indexPattern.id, columns: { [newId]: newColumn }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 7a36d52ad897b..6161df1167afe 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -42,6 +42,7 @@ export const dateHistogramOperation: OperationDefinition { if ( type === 'date' && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx index 29e5787fa4f54..7eb10456b2a6e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx @@ -53,6 +53,7 @@ export const termsOperation: OperationDefinition = { displayName: i18n.translate('xpack.lens.indexPattern.terms', { defaultMessage: 'Top values', }), + priority: 3, // Higher than any metric getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( supportedTypes.has(type) && diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 111b1040de989..e5d20839aae3d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -5,8 +5,7 @@ */ import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColumn } from './index'; -import { AvgIndexPatternColumn, MinIndexPatternColumn } from './definitions/metrics'; -import { CountIndexPatternColumn } from './definitions/count'; +import { AvgIndexPatternColumn } from './definitions/metrics'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; @@ -197,33 +196,21 @@ describe('getOperationTypesForField', () => { expect(column.operationType).toEqual('avg'); expect(column.sourceField).toEqual(field.name); }); - - it('should pick a suitable field operation if none is passed in', () => { - const field = expectedIndexPatterns[1].fields[1]; - const column = buildColumn({ - layerId: 'first', - indexPattern: expectedIndexPatterns[1], - columns: state.layers.first.columns, - suggestedPriority: 0, - field, - }) as MinIndexPatternColumn; - expect(column.operationType).toEqual('avg'); - expect(column.sourceField).toEqual(field.name); - }); - - it('should pick a suitable document operation if none is passed in', () => { - const column = buildColumn({ - layerId: 'first', - indexPattern: expectedIndexPatterns[1], - columns: state.layers.first.columns, - suggestedPriority: 0, - field: documentField, - }) as CountIndexPatternColumn; - expect(column.operationType).toEqual('count'); - }); }); describe('getAvailableOperationsByMetaData', () => { + it('should put the average operation first', () => { + const numberOperation = getAvailableOperationsByMetadata(expectedIndexPatterns[1]).find( + ({ operationMetaData }) => + !operationMetaData.isBucketed && operationMetaData.dataType === 'number' + )!; + expect(numberOperation.operations[0]).toEqual( + expect.objectContaining({ + operationType: 'avg', + }) + ); + }); + it('should list out all field-operation tuples for different operation meta data', () => { expect(getAvailableOperationsByMetadata(expectedIndexPatterns[1])).toMatchInlineSnapshot(` Array [ @@ -278,17 +265,22 @@ describe('getOperationTypesForField', () => { "operations": Array [ Object { "field": "bytes", - "operationType": "min", + "operationType": "avg", "type": "field", }, Object { "field": "bytes", - "operationType": "max", + "operationType": "sum", "type": "field", }, Object { "field": "bytes", - "operationType": "avg", + "operationType": "min", + "type": "field", + }, + Object { + "field": "bytes", + "operationType": "max", "type": "field", }, Object { @@ -306,11 +298,6 @@ describe('getOperationTypesForField', () => { "operationType": "cardinality", "type": "field", }, - Object { - "field": "bytes", - "operationType": "sum", - "type": "field", - }, ], }, ] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index ce8ea55c445dc..dbcd4eac7fd59 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -52,20 +52,22 @@ export function getOperationDisplay() { return display; } +function getSortScoreByPriority(a: GenericOperationDefinition, b: GenericOperationDefinition) { + return (b.priority || Number.NEGATIVE_INFINITY) - (a.priority || Number.NEGATIVE_INFINITY); +} + /** * Returns all `OperationType`s that can build a column using `buildColumn` based on the * passed in field. */ -export function getOperationTypesForField(field: IndexPatternField) { +export function getOperationTypesForField(field: IndexPatternField): OperationType[] { return operationDefinitions .filter( operationDefinition => 'getPossibleOperationForField' in operationDefinition && operationDefinition.getPossibleOperationForField(field) ) - .sort( - (a, b) => (b.priority || Number.NEGATIVE_INFINITY) - (a.priority || Number.NEGATIVE_INFINITY) - ) + .sort(getSortScoreByPriority) .map(({ type }) => type); } @@ -131,7 +133,7 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { } }; - operationDefinitions.forEach(operationDefinition => { + operationDefinitions.sort(getSortScoreByPriority).forEach(operationDefinition => { indexPattern.fields.forEach(field => { addToMap( { @@ -156,13 +158,6 @@ function getPossibleOperationForField( : undefined; } -function getDefinition(findFunction: (definition: GenericOperationDefinition) => boolean) { - const candidates = operationDefinitions.filter(findFunction); - return candidates.reduce((a, b) => - (a.priority || Number.NEGATIVE_INFINITY) > (b.priority || Number.NEGATIVE_INFINITY) ? a : b - ); -} - /** * Changes the field of the passed in colum. To do so, this method uses the `onFieldChange` function of * the operation definition of the column. Returns a new column object with the field changed. @@ -204,7 +199,7 @@ export function buildColumn({ suggestedPriority, previousColumn, }: { - op?: OperationType; + op: OperationType; columns: Partial>; suggestedPriority: DimensionPriority | undefined; layerId: string; @@ -212,15 +207,7 @@ export function buildColumn({ field: IndexPatternField; previousColumn?: IndexPatternColumn; }): IndexPatternColumn { - let operationDefinition: GenericOperationDefinition | undefined; - - if (op) { - operationDefinition = operationDefinitionMap[op]; - } else if (field) { - operationDefinition = getDefinition(definition => - Boolean(getPossibleOperationForField(definition, field)) - ); - } + const operationDefinition = operationDefinitionMap[op]; if (!operationDefinition) { throw new Error('No suitable operation found for given parameters'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 3747deaa6059b..3ab51b5fa3f2b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -5,6 +5,7 @@ */ import _ from 'lodash'; +import { Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { IndexPatternColumn } from './indexpattern'; import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState } from './types'; @@ -14,7 +15,7 @@ function getExpressionForLayer( indexPattern: IndexPattern, columns: Record, columnOrder: string[] -) { +): Ast | null { if (columnOrder.length === 0) { return null; } @@ -40,29 +41,70 @@ function getExpressionForLayer( }; }, {} as Record); - const formatterOverrides = columnEntries - .map(([id, col]) => { - const format = col.params && 'format' in col.params ? col.params.format : undefined; - if (!format) { - return null; - } - const base = `| lens_format_column format="${format.id}" columnId="${id}"`; - if (typeof format.params?.decimals === 'number') { - return base + ` decimals=${format.params.decimals}`; - } - return base; - }) - .filter(expr => !!expr) - .join(' '); + type FormattedColumn = Required>; - return `esaggs - index="${indexPattern.id}" - metricsAtAllLevels=false - partialRows=false - includeFormatHints=true - aggConfigs={lens_auto_date aggConfigs='${JSON.stringify( - aggs - )}'} | lens_rename_columns idMap='${JSON.stringify(idMap)}' ${formatterOverrides}`; + const columnsWithFormatters = columnEntries.filter( + ([, col]) => col.params && 'format' in col.params && col.params.format + ) as Array<[string, FormattedColumn]>; + const formatterOverrides: ExpressionFunctionAST[] = columnsWithFormatters.map(([id, col]) => { + const format = (col as FormattedColumn).params!.format; + const base: ExpressionFunctionAST = { + type: 'function', + function: 'lens_format_column', + arguments: { + format: [format.id], + columnId: [id], + }, + }; + if (typeof format.params?.decimals === 'number') { + return { + ...base, + arguments: { + ...base.arguments, + decimals: [format.params.decimals], + }, + }; + } + return base; + }); + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'esaggs', + arguments: { + index: [indexPattern.id], + metricsAtAllLevels: [false], + partialRows: [false], + includeFormatHints: [true], + aggConfigs: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_auto_date', + arguments: { + aggConfigs: [JSON.stringify(aggs)], + }, + }, + ], + }, + ], + }, + }, + { + type: 'function', + function: 'lens_rename_columns', + arguments: { + idMap: [JSON.stringify(idMap)], + }, + }, + ...formatterOverrides, + ], + }; } return null; diff --git a/x-pack/plugins/lens/server/saved_objects.ts b/x-pack/plugins/lens/server/saved_objects.ts index b2eb325c09266..ac80eb098e780 100644 --- a/x-pack/plugins/lens/server/saved_objects.ts +++ b/x-pack/plugins/lens/server/saved_objects.ts @@ -12,7 +12,7 @@ export function setupSavedObjects(core: CoreSetup) { core.savedObjects.registerType({ name: 'lens', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', management: { icon: 'lensApp', defaultSearchField: 'title', @@ -46,7 +46,7 @@ export function setupSavedObjects(core: CoreSetup) { core.savedObjects.registerType({ name: 'lens-ui-telemetry', hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { name: { diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 1468bf7fdc1f2..a4006732224ce 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -1,9 +1,3 @@ -/* - * 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. - */ - /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License; @@ -46,9 +40,10 @@ export function createMapPath(id: string) { export enum LAYER_TYPE { TILE = 'TILE', VECTOR = 'VECTOR', - VECTOR_TILE = 'VECTOR_TILE', + VECTOR_TILE = 'VECTOR_TILE', // for static display of mvt vector tiles with a mapbox stylesheet. Does not support any ad-hoc configurations. Used for consuming EMS vector tiles. HEATMAP = 'HEATMAP', BLENDED_VECTOR = 'BLENDED_VECTOR', + TILED_VECTOR = 'TILED_VECTOR', // similar to a regular vector-layer, but it consumes the data as .mvt tilea iso GeoJson. It supports similar ad-hoc configurations like a regular vector layer (E.g. using IVectorStyle), although there is some loss of functionality e.g. does not support term joining } export enum SORT_ORDER { @@ -67,12 +62,14 @@ export enum SOURCE_TYPES { KIBANA_TILEMAP = 'KIBANA_TILEMAP', REGIONMAP_FILE = 'REGIONMAP_FILE', GEOJSON_FILE = 'GEOJSON_FILE', + MVT_SINGLE_LAYER = 'MVT_SINGLE_LAYER', } export enum FIELD_ORIGIN { SOURCE = 'source', JOIN = 'join', } +export const JOIN_FIELD_NAME_PREFIX = '__kbnjoin__'; export const SOURCE_DATA_ID_ORIGIN = 'source'; export const META_ID_ORIGIN_SUFFIX = 'meta'; @@ -130,6 +127,7 @@ export enum DRAW_TYPE { POLYGON = 'POLYGON', } +export const AGG_DELIMITER = '_of_'; export enum AGG_TYPE { AVG = 'avg', COUNT = 'count', diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts index 26044d28d53a3..e94dc6694b38d 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts @@ -31,12 +31,12 @@ type ESGeoGridSourceSyncMeta = { requestType: RENDER_AS; }; -export type VectorSourceSyncMeta = ESSearchSourceSyncMeta | ESGeoGridSourceSyncMeta; +export type VectorSourceSyncMeta = ESSearchSourceSyncMeta | ESGeoGridSourceSyncMeta | null; export type VectorSourceRequestMeta = MapFilters & { applyGlobalQuery: boolean; fieldNames: string[]; - geogridPrecision: number; + geogridPrecision?: number; sourceQuery: MapQuery; sourceMeta: VectorSourceSyncMeta; }; diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts index ff285877750c5..f8175b0ed3f10 100644 --- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts @@ -94,6 +94,18 @@ export type XYZTMSSourceDescriptor = AbstractSourceDescriptor & urlTemplate: string; }; +export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor & { + urlTemplate: string; + layerName: string; + + // These are the min/max zoom levels of the availability of the a particular layerName in the tileset at urlTemplate. + // These are _not_ the visible zoom-range of the data on a map. + // Tiled data can be displayed at higher levels of zoom than that they are stored in the tileset. + // e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels + minSourceZoom: number; + maxSourceZoom: number; +}; + export type JoinDescriptor = { leftField: string; right: ESTermSourceDescriptor; @@ -107,7 +119,9 @@ export type SourceDescriptor = | ESTermSourceDescriptor | ESSearchSourceDescriptor | ESGeoGridSourceDescriptor - | EMSFileSourceDescriptor; + | EMSFileSourceDescriptor + | ESPewPewSourceDescriptor + | TiledSingleLayerVectorSourceDescriptor; export type LayerDescriptor = { __dataRequests?: DataRequestDescriptor[]; diff --git a/x-pack/plugins/maps/common/get_join_key.ts b/x-pack/plugins/maps/common/get_join_key.ts new file mode 100644 index 0000000000000..f1ee95126b9a9 --- /dev/null +++ b/x-pack/plugins/maps/common/get_join_key.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AGG_DELIMITER, AGG_TYPE, JOIN_FIELD_NAME_PREFIX } from './constants'; + +// function in common since its needed by migration +export function getJoinAggKey({ + aggType, + aggFieldName, + rightSourceId, +}: { + aggType: AGG_TYPE; + aggFieldName?: string; + rightSourceId: string; +}) { + const metricKey = + aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${aggFieldName}` : aggType; + return `${JOIN_FIELD_NAME_PREFIX}${metricKey}__${rightSourceId}`; +} diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js index a54df69471aa0..92fcf01f3901f 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js @@ -63,13 +63,13 @@ export class AddLayerPanel extends Component { return; } - const style = + const styleDescriptor = this.state.layer && this.state.layer.getCurrentStyle() ? this.state.layer.getCurrentStyle().getDescriptor() : null; const layerInitProps = { ...options, - style: style, + style: styleDescriptor, }; const newLayer = source.createDefaultLayer(layerInitProps, this.props.mapColors); if (!this._isMounted) { diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js index 73c98db8e429d..e8f980bbbf2b4 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js @@ -13,10 +13,13 @@ import { updateLayerMinZoom, updateLayerAlpha, } from '../../../actions/map_actions'; +import { MAX_ZOOM } from '../../../../../../../plugins/maps/common/constants'; function mapStateToProps(state = {}) { const selectedLayer = getSelectedLayer(state); return { + minVisibilityZoom: selectedLayer.getMinSourceZoom(), + maxVisibilityZoom: MAX_ZOOM, alpha: selectedLayer.getAlpha(), label: selectedLayer.getLabel(), layerId: selectedLayer.getId(), diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js index 3a3db78bbf1c8..c2e567e82d32b 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js @@ -13,8 +13,6 @@ import { ValidatedRange } from '../../../components/validated_range'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; -import { MAX_ZOOM, MIN_ZOOM } from '../../../../../../legacy/plugins/maps/common/constants'; - export function LayerSettings(props) { const onLabelChange = event => { const label = event.target.value; @@ -22,8 +20,8 @@ export function LayerSettings(props) { }; const onZoomChange = ([min, max]) => { - props.updateMinZoom(props.layerId, Math.max(MIN_ZOOM, parseInt(min, 10))); - props.updateMaxZoom(props.layerId, Math.min(MAX_ZOOM, parseInt(max, 10))); + props.updateMinZoom(props.layerId, Math.max(props.minVisibilityZoom, parseInt(min, 10))); + props.updateMaxZoom(props.layerId, Math.min(props.maxVisibilityZoom, parseInt(max, 10))); }; const onAlphaChange = alpha => { @@ -38,8 +36,8 @@ export function LayerSettings(props) { defaultMessage: 'Visibility', })} formRowDisplay="columnCompressed" - min={MIN_ZOOM} - max={MAX_ZOOM} + min={props.minVisibilityZoom} + max={props.maxVisibilityZoom} value={[props.minZoom, props.maxZoom]} showInput="inputWithPopover" showRange diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js index 35d52a8d0f1a0..9480411b6344e 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -16,6 +16,8 @@ import { import { getGlyphUrl, isRetina } from '../../../meta'; import { DECIMAL_DEGREES_PRECISION, + MAX_ZOOM, + MIN_ZOOM, ZOOM_PRECISION, } from '../../../../../../legacy/plugins/maps/common/constants'; import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; @@ -135,6 +137,8 @@ export class MBMapContainer extends React.Component { scrollZoom: this.props.scrollZoom, preserveDrawingBuffer: getInjectedVarFunc()('preserveDrawingBuffer', false), interactive: !this.props.disableInteractive, + minZoom: MIN_ZOOM, + maxZoom: MAX_ZOOM, }; const initialView = _.get(this.props.goto, 'center'); if (initialView) { diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index 2a79314380330..dcbd54a09381f 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -38,7 +38,9 @@ export const getFileUploadComponent = () => { }; let getInjectedVar; -export const setInjectedVarFunc = getInjectedVarFunc => (getInjectedVar = getInjectedVarFunc); +export const setInjectedVarFunc = getInjectedVarFunc => { + getInjectedVar = getInjectedVarFunc; +}; export const getInjectedVarFunc = () => getInjectedVar; let uiSettings; diff --git a/x-pack/plugins/maps/public/layers/joins/inner_join.test.js b/x-pack/plugins/maps/public/layers/joins/inner_join.test.js index 65c37860ffa18..f197a67becfae 100644 --- a/x-pack/plugins/maps/public/layers/joins/inner_join.test.js +++ b/x-pack/plugins/maps/public/layers/joins/inner_join.test.js @@ -35,7 +35,7 @@ const leftJoin = new InnerJoin( }, mockSource ); -const COUNT_PROPERTY_NAME = '__kbnjoin__count_groupby_kibana_sample_data_logs.geo.dest'; +const COUNT_PROPERTY_NAME = '__kbnjoin__count__d3625663-5b34-4d50-a784-0d743f676a0c'; describe('joinPropertiesToFeature', () => { it('Should add join property to features in feature collection', () => { diff --git a/x-pack/plugins/maps/public/layers/layer.d.ts b/x-pack/plugins/maps/public/layers/layer.d.ts index 8fb69734d3d06..e8fc5d473626c 100644 --- a/x-pack/plugins/maps/public/layers/layer.d.ts +++ b/x-pack/plugins/maps/public/layers/layer.d.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { LayerDescriptor, MapExtent, MapFilters } from '../../common/descriptor_types'; +import { LayerDescriptor, MapExtent, MapFilters, MapQuery } from '../../common/descriptor_types'; import { ISource } from './sources/source'; import { DataRequest } from './util/data_request'; import { SyncContext } from '../actions/map_actions'; @@ -17,6 +17,11 @@ export interface ILayer { getSource(): ISource; getSourceForEditing(): ISource; syncData(syncContext: SyncContext): Promise; + isVisible(): boolean; + showAtZoomLevel(zoomLevel: number): boolean; + getMinZoom(): number; + getMaxZoom(): number; + getMinSourceZoom(): number; } export interface ILayerArguments { @@ -35,4 +40,12 @@ export class AbstractLayer implements ILayer { getSource(): ISource; getSourceForEditing(): ISource; syncData(syncContext: SyncContext): Promise; + isVisible(): boolean; + showAtZoomLevel(zoomLevel: number): boolean; + getMinZoom(): number; + getMaxZoom(): number; + getMinSourceZoom(): number; + getQuery(): MapQuery; + _removeStaleMbSourcesAndLayers(mbMap: unknown): void; + _requiresPrevSourceCleanup(mbMap: unknown): boolean; } diff --git a/x-pack/plugins/maps/public/layers/layer.js b/x-pack/plugins/maps/public/layers/layer.js index 26bce872b3c2c..19dcbaf1dfcfd 100644 --- a/x-pack/plugins/maps/public/layers/layer.js +++ b/x-pack/plugins/maps/public/layers/layer.js @@ -141,7 +141,8 @@ export class AbstractLayer { defaultMessage: `Layer is hidden.`, }); } else if (!this.showAtZoomLevel(zoomLevel)) { - const { minZoom, maxZoom } = this.getZoomConfig(); + const minZoom = this.getMinZoom(); + const maxZoom = this.getMaxZoom(); icon = ; tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', { defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`, @@ -203,7 +204,7 @@ export class AbstractLayer { } showAtZoomLevel(zoom) { - return zoom >= this._descriptor.minZoom && zoom <= this._descriptor.maxZoom; + return zoom >= this.getMinZoom() && zoom <= this.getMaxZoom(); } getMinZoom() { @@ -214,6 +215,30 @@ export class AbstractLayer { return this._descriptor.maxZoom; } + getMinSourceZoom() { + return this._source.getMinZoom(); + } + + _requiresPrevSourceCleanup() { + return false; + } + + _removeStaleMbSourcesAndLayers(mbMap) { + if (this._requiresPrevSourceCleanup(mbMap)) { + const mbStyle = mbMap.getStyle(); + mbStyle.layers.forEach(mbLayer => { + if (this.ownsMbLayerId(mbLayer.id)) { + mbMap.removeLayer(mbLayer.id); + } + }); + Object.keys(mbStyle.sources).some(mbSourceId => { + if (this.ownsMbSourceId(mbSourceId)) { + mbMap.removeSource(mbSourceId); + } + }); + } + } + getAlpha() { return this._descriptor.alpha; } @@ -222,13 +247,6 @@ export class AbstractLayer { return this._descriptor.query; } - getZoomConfig() { - return { - minZoom: this._descriptor.minZoom, - maxZoom: this._descriptor.maxZoom, - }; - } - getCurrentStyle() { return this._style; } diff --git a/x-pack/plugins/maps/public/layers/load_layer_wizards.js b/x-pack/plugins/maps/public/layers/load_layer_wizards.js deleted file mode 100644 index d0169165eaa35..0000000000000 --- a/x-pack/plugins/maps/public/layers/load_layer_wizards.js +++ /dev/null @@ -1,30 +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 { registerLayerWizard } from './layer_wizard_registry'; -import { uploadLayerWizardConfig } from './sources/client_file_source'; -import { esDocumentsLayerWizardConfig } from './sources/es_search_source'; -import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from './sources/es_geo_grid_source'; -import { point2PointLayerWizardConfig } from './sources/es_pew_pew_source/es_pew_pew_source'; -import { emsBoundariesLayerWizardConfig } from './sources/ems_file_source'; -import { emsBaseMapLayerWizardConfig } from './sources/ems_tms_source'; -import { kibanaRegionMapLayerWizardConfig } from './sources/kibana_regionmap_source'; -import { kibanaBasemapLayerWizardConfig } from './sources/kibana_tilemap_source'; -import { tmsLayerWizardConfig } from './sources/xyz_tms_source'; -import { wmsLayerWizardConfig } from './sources/wms_source'; - -// Registration order determines display order -registerLayerWizard(uploadLayerWizardConfig); -registerLayerWizard(esDocumentsLayerWizardConfig); -registerLayerWizard(clustersLayerWizardConfig); -registerLayerWizard(heatmapLayerWizardConfig); -registerLayerWizard(point2PointLayerWizardConfig); -registerLayerWizard(emsBoundariesLayerWizardConfig); -registerLayerWizard(emsBaseMapLayerWizardConfig); -registerLayerWizard(kibanaRegionMapLayerWizardConfig); -registerLayerWizard(kibanaBasemapLayerWizardConfig); -registerLayerWizard(tmsLayerWizardConfig); -registerLayerWizard(wmsLayerWizardConfig); diff --git a/x-pack/plugins/maps/public/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/layers/load_layer_wizards.ts new file mode 100644 index 0000000000000..49d128257fe20 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/load_layer_wizards.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { registerLayerWizard } from './layer_wizard_registry'; +// @ts-ignore +import { uploadLayerWizardConfig } from './sources/client_file_source'; +// @ts-ignore +import { esDocumentsLayerWizardConfig } from './sources/es_search_source'; +// @ts-ignore +import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from './sources/es_geo_grid_source'; +// @ts-ignore +import { point2PointLayerWizardConfig } from './sources/es_pew_pew_source/es_pew_pew_source'; +// @ts-ignore +import { emsBoundariesLayerWizardConfig } from './sources/ems_file_source'; +// @ts-ignore +import { emsBaseMapLayerWizardConfig } from './sources/ems_tms_source'; +// @ts-ignore +import { kibanaRegionMapLayerWizardConfig } from './sources/kibana_regionmap_source'; +// @ts-ignore +import { kibanaBasemapLayerWizardConfig } from './sources/kibana_tilemap_source'; +import { tmsLayerWizardConfig } from './sources/xyz_tms_source'; +// @ts-ignore +import { wmsLayerWizardConfig } from './sources/wms_source'; +import { mvtVectorSourceWizardConfig } from './sources/mvt_single_layer_vector_source'; +// @ts-ignore +import { getInjectedVarFunc } from '../kibana_services'; + +// Registration order determines display order +let registered = false; +export function registerLayerWizards() { + if (registered) { + return; + } + // @ts-ignore + registerLayerWizard(uploadLayerWizardConfig); + // @ts-ignore + registerLayerWizard(esDocumentsLayerWizardConfig); + // @ts-ignore + registerLayerWizard(clustersLayerWizardConfig); + // @ts-ignore + registerLayerWizard(heatmapLayerWizardConfig); + // @ts-ignore + registerLayerWizard(point2PointLayerWizardConfig); + // @ts-ignore + registerLayerWizard(emsBoundariesLayerWizardConfig); + // @ts-ignore + registerLayerWizard(emsBaseMapLayerWizardConfig); + // @ts-ignore + registerLayerWizard(kibanaRegionMapLayerWizardConfig); + // @ts-ignore + registerLayerWizard(kibanaBasemapLayerWizardConfig); + registerLayerWizard(tmsLayerWizardConfig); + // @ts-ignore + registerLayerWizard(wmsLayerWizardConfig); + + const getInjectedVar = getInjectedVarFunc(); + if (getInjectedVar && getInjectedVar('enableVectorTiles', false)) { + // eslint-disable-next-line no-console + console.warn('Vector tiles are an experimental feature and should not be used in production.'); + registerLayerWizard(mvtVectorSourceWizardConfig); + } + registered = true; +} diff --git a/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.js b/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.js index c6197f137f212..58c56fe32f766 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.js @@ -7,16 +7,14 @@ import { i18n } from '@kbn/i18n'; import { AbstractESSource } from '../es_source'; import { esAggFieldsFactory } from '../../fields/es_agg_field'; - import { + AGG_DELIMITER, AGG_TYPE, COUNT_PROP_LABEL, COUNT_PROP_NAME, FIELD_ORIGIN, } from '../../../../common/constants'; -export const AGG_DELIMITER = '_of_'; - export class AbstractESAggSource extends AbstractESSource { constructor(descriptor, inspectorAdapters) { super(descriptor, inspectorAdapters); diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts index 3f596cea1ae39..96347c444dd5b 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts @@ -23,6 +23,7 @@ export class ESGeoGridSource extends AbstractESAggSource { constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown); + getFieldNames(): string[]; getGridResolution(): GRID_RESOLUTION; getGeoGridPrecision(zoom: number): number; } diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts index 0a4e48a195ec6..c904280a38c85 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts @@ -9,4 +9,5 @@ import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; export class ESSearchSource extends AbstractESSource { constructor(sourceDescriptor: Partial, inspectorAdapters: unknown); + getFieldNames(): string[]; } diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.js b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.js index 826197cc4fec7..cb07bb0e7d2ed 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.js @@ -7,15 +7,14 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_MAX_BUCKETS_LIMIT, FIELD_ORIGIN, AGG_TYPE } from '../../../../common/constants'; +import { AGG_TYPE, DEFAULT_MAX_BUCKETS_LIMIT, FIELD_ORIGIN } from '../../../../common/constants'; +import { getJoinAggKey } from '../../../../common/get_join_key'; import { ESDocField } from '../../fields/es_doc_field'; -import { AbstractESAggSource, AGG_DELIMITER } from '../es_agg_source'; +import { AbstractESAggSource } from '../es_agg_source'; import { getField, addFieldToDSL, extractPropertiesFromBucket } from '../../util/es_agg_utils'; const TERMS_AGG_NAME = 'join'; -const FIELD_NAME_PREFIX = '__kbnjoin__'; -const GROUP_BY_DELIMITER = '_groupby_'; const TERMS_BUCKET_KEYS_TO_IGNORE = ['key', 'doc_count']; export function extractPropertiesMap(rawEsData, countPropertyName) { @@ -64,11 +63,11 @@ export class ESTermSource extends AbstractESAggSource { } getAggKey(aggType, fieldName) { - const metricKey = - aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${fieldName}` : aggType; - return `${FIELD_NAME_PREFIX}${metricKey}${GROUP_BY_DELIMITER}${ - this._descriptor.indexPatternTitle - }.${this._termField.getName()}`; + return getJoinAggKey({ + aggType, + aggFieldName: fieldName, + rightSourceId: this._descriptor.id, + }); } getAggLabel(aggType, fieldName) { diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.test.js b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.test.js index b6cd3b670d3ce..14eb39180a6b8 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.test.js +++ b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.test.js @@ -32,33 +32,32 @@ const metricExamples = [ describe('getMetricFields', () => { it('should override name and label of count metric', async () => { const source = new ESTermSource({ + id: '1234', indexPatternTitle: indexPatternTitle, term: termFieldName, }); const metrics = source.getMetricFields(); - expect(metrics[0].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField'); + expect(metrics[0].getName()).toEqual('__kbnjoin__count__1234'); expect(await metrics[0].getLabel()).toEqual('Count of myIndex'); }); it('should override name and label of sum metric', async () => { const source = new ESTermSource({ + id: '1234', indexPatternTitle: indexPatternTitle, term: termFieldName, metrics: metricExamples, }); const metrics = source.getMetricFields(); - expect(metrics[0].getName()).toEqual( - '__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField' - ); + expect(metrics[0].getName()).toEqual('__kbnjoin__sum_of_myFieldGettingSummed__1234'); expect(await metrics[0].getLabel()).toEqual('my custom label'); - expect(metrics[1].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField'); + expect(metrics[1].getName()).toEqual('__kbnjoin__count__1234'); expect(await metrics[1].getLabel()).toEqual('Count of myIndex'); }); }); describe('extractPropertiesMap', () => { - const minPropName = - '__kbnjoin__min_of_avlAirTemp_groupby_kibana_sample_data_ky_avl.kytcCountyNmbr'; + const minPropName = '__kbnjoin__min_of_avlAirTemp__1234'; const responseWithNumberTypes = { aggregations: { join: { @@ -81,7 +80,7 @@ describe('extractPropertiesMap', () => { }, }, }; - const countPropName = '__kbnjoin__count_groupby_kibana_sample_data_ky_avl.kytcCountyNmbr'; + const countPropName = '__kbnjoin__count__1234'; let propertiesMap; beforeAll(() => { diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/index.ts b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/index.ts new file mode 100644 index 0000000000000..89b7e76a7e359 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './mvt_single_layer_vector_source'; +export * from './layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx new file mode 100644 index 0000000000000..dfdea1489d50c --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { + MVTSingleLayerVectorSourceEditor, + MVTSingleLayerVectorSourceConfig, +} from './mvt_single_layer_vector_source_editor'; +import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { SOURCE_TYPES } from '../../../../common/constants'; + +export const mvtVectorSourceWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { + defaultMessage: 'Vector source wizard', + }), + icon: 'grid', + renderWizard: ({ onPreviewSource, inspectorAdapters }: RenderWizardArguments) => { + const onSourceConfigChange = ({ + urlTemplate, + layerName, + minSourceZoom, + maxSourceZoom, + }: MVTSingleLayerVectorSourceConfig) => { + const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor({ + urlTemplate, + layerName, + minSourceZoom, + maxSourceZoom, + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + }); + const source = new MVTSingleLayerVectorSource(sourceDescriptor, inspectorAdapters); + onPreviewSource(source); + }; + return ; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts new file mode 100644 index 0000000000000..0bfda6be72203 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -0,0 +1,175 @@ +/* + * 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 uuid from 'uuid/v4'; +import { AbstractSource, ImmutableSourceProperty } from '../source'; +import { TiledVectorLayer } from '../../tiled_vector_layer'; +import { GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; +import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES } from '../../../../common/constants'; +import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; +import { IField } from '../../fields/field'; +import { registerSource } from '../source_registry'; +import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; +import { + LayerDescriptor, + MapExtent, + TiledSingleLayerVectorSourceDescriptor, + VectorSourceRequestMeta, + VectorSourceSyncMeta, +} from '../../../../common/descriptor_types'; +import { VectorLayerArguments } from '../../vector_layer'; + +export const sourceTitle = i18n.translate( + 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', + { + defaultMessage: 'Vector Tile Layer', + } +); + +export class MVTSingleLayerVectorSource extends AbstractSource + implements ITiledSingleLayerVectorSource { + static createDescriptor({ + urlTemplate, + layerName, + minSourceZoom, + maxSourceZoom, + }: TiledSingleLayerVectorSourceDescriptor) { + return { + type: SOURCE_TYPES.MVT_SINGLE_LAYER, + id: uuid(), + urlTemplate, + layerName, + minSourceZoom: Math.max(MIN_ZOOM, minSourceZoom), + maxSourceZoom: Math.min(MAX_ZOOM, maxSourceZoom), + }; + } + + readonly _descriptor: TiledSingleLayerVectorSourceDescriptor; + + constructor( + sourceDescriptor: TiledSingleLayerVectorSourceDescriptor, + inspectorAdapters?: object + ) { + super(sourceDescriptor, inspectorAdapters); + this._descriptor = sourceDescriptor; + } + + renderSourceSettingsEditor() { + return null; + } + + getFieldNames(): string[] { + return []; + } + + createDefaultLayer(options: LayerDescriptor): TiledVectorLayer { + const layerDescriptor = { + sourceDescriptor: this._descriptor, + ...options, + }; + const normalizedLayerDescriptor = TiledVectorLayer.createDescriptor(layerDescriptor, []); + const vectorLayerArguments: VectorLayerArguments = { + layerDescriptor: normalizedLayerDescriptor, + source: this, + }; + return new TiledVectorLayer(vectorLayerArguments); + } + + getGeoJsonWithMeta( + layerName: 'string', + searchFilters: unknown[], + registerCancelCallback: (callback: () => void) => void + ): Promise { + // todo: remove this method + // This is a consequence of ITiledSingleLayerVectorSource extending IVectorSource. + throw new Error('Does not implement getGeoJsonWithMeta'); + } + + async getFields(): Promise { + return []; + } + + async getImmutableProperties(): Promise { + return [ + { label: getDataSourceLabel(), value: sourceTitle }, + { label: getUrlLabel(), value: this._descriptor.urlTemplate }, + { + label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage', { + defaultMessage: 'Layer name', + }), + value: this._descriptor.layerName, + }, + { + label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage', { + defaultMessage: 'Min zoom', + }), + value: this._descriptor.minSourceZoom.toString(), + }, + { + label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage', { + defaultMessage: 'Max zoom', + }), + value: this._descriptor.maxSourceZoom.toString(), + }, + ]; + } + + async getDisplayName(): Promise { + return this._descriptor.layerName; + } + + async getUrlTemplateWithMeta() { + return { + urlTemplate: this._descriptor.urlTemplate, + layerName: this._descriptor.layerName, + minSourceZoom: this._descriptor.minSourceZoom, + maxSourceZoom: this._descriptor.maxSourceZoom, + }; + } + + async getSupportedShapeTypes(): Promise { + return [VECTOR_SHAPE_TYPES.POINT, VECTOR_SHAPE_TYPES.LINE, VECTOR_SHAPE_TYPES.POLYGON]; + } + + canFormatFeatureProperties() { + return false; + } + + getMinZoom() { + return this._descriptor.minSourceZoom; + } + + getMaxZoom() { + return this._descriptor.maxSourceZoom; + } + + getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent { + return { + maxLat: 90, + maxLon: 180, + minLat: -90, + minLon: -180, + }; + } + + getFieldByName(fieldName: string): IField | null { + return null; + } + + getSyncMeta(): VectorSourceSyncMeta { + return null; + } + + getApplyGlobalQuery(): boolean { + return false; + } +} + +registerSource({ + ConstructorFunction: MVTSingleLayerVectorSource, + type: SOURCE_TYPES.MVT_SINGLE_LAYER, +}); diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx new file mode 100644 index 0000000000000..7a4b8d43811da --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx @@ -0,0 +1,128 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import React, { Fragment, Component, ChangeEvent } from 'react'; +import _ from 'lodash'; +import { EuiFieldText, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; +import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public'; + +export type MVTSingleLayerVectorSourceConfig = { + urlTemplate: string; + layerName: string; + minSourceZoom: number; + maxSourceZoom: number; +}; + +export interface Props { + onSourceConfigChange: (sourceConfig: MVTSingleLayerVectorSourceConfig) => void; +} + +interface State { + urlTemplate: string; + layerName: string; + minSourceZoom: number; + maxSourceZoom: number; +} + +export class MVTSingleLayerVectorSourceEditor extends Component { + state = { + urlTemplate: '', + layerName: '', + minSourceZoom: MIN_ZOOM, + maxSourceZoom: MAX_ZOOM, + }; + + _sourceConfigChange = _.debounce(() => { + const canPreview = + this.state.urlTemplate.indexOf('{x}') >= 0 && + this.state.urlTemplate.indexOf('{y}') >= 0 && + this.state.urlTemplate.indexOf('{z}') >= 0; + + if (canPreview && this.state.layerName) { + this.props.onSourceConfigChange({ + urlTemplate: this.state.urlTemplate, + layerName: this.state.layerName, + minSourceZoom: this.state.minSourceZoom, + maxSourceZoom: this.state.maxSourceZoom, + }); + } + }, 200); + + _handleUrlTemplateChange = (e: ChangeEvent) => { + const url = e.target.value; + this.setState( + { + urlTemplate: url, + }, + () => this._sourceConfigChange() + ); + }; + + _handleLayerNameInputChange = (e: ChangeEvent) => { + const layerName = e.target.value; + this.setState( + { + layerName, + }, + () => this._sourceConfigChange() + ); + }; + + _handleZoomRangeChange = (e: Value) => { + const minSourceZoom = parseInt(e[0] as string, 10); + const maxSourceZoom = parseInt(e[1] as string, 10); + + if (this.state.minSourceZoom !== minSourceZoom || this.state.maxSourceZoom !== maxSourceZoom) { + this.setState({ minSourceZoom, maxSourceZoom }, () => this._sourceConfigChange()); + } + }; + + render() { + return ( + + + + + + + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/layers/sources/source.d.ts b/x-pack/plugins/maps/public/layers/sources/source.d.ts index a1581b826d9a6..5a01da02adaae 100644 --- a/x-pack/plugins/maps/public/layers/sources/source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/source.d.ts @@ -19,7 +19,7 @@ export type Attribution = { }; export interface ISource { - createDefaultLayer(): ILayer; + createDefaultLayer(options?: LayerDescriptor): ILayer; destroy(): void; getDisplayName(): Promise; getInspectorAdapters(): object; @@ -31,6 +31,8 @@ export interface ISource { isTimeAware(): Promise; getImmutableProperties(): Promise; getAttributions(): Promise; + getMinZoom(): number; + getMaxZoom(): number; } export class AbstractSource implements ISource { @@ -49,4 +51,6 @@ export class AbstractSource implements ISource { isTimeAware(): Promise; getImmutableProperties(): Promise; getAttributions(): Promise; + getMinZoom(): number; + getMaxZoom(): number; } diff --git a/x-pack/plugins/maps/public/layers/sources/source.js b/x-pack/plugins/maps/public/layers/sources/source.js index 3029a5c091202..555b8999d6284 100644 --- a/x-pack/plugins/maps/public/layers/sources/source.js +++ b/x-pack/plugins/maps/public/layers/sources/source.js @@ -6,6 +6,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { copyPersistentState } from '../../reducers/util'; +import { MIN_ZOOM, MAX_ZOOM } from '../../../common/constants'; export class AbstractSource { static isIndexingSource = false; @@ -79,7 +80,7 @@ export class AbstractSource { return false; } - isQueryAware() { + async isTimeAware() { return false; } @@ -107,6 +108,14 @@ export class AbstractSource { return []; } + isFilterByMapBounds() { + return false; + } + + isQueryAware() { + return false; + } + getGeoGridPrecision() { return 0; } @@ -140,4 +149,12 @@ export class AbstractSource { async getValueSuggestions(/* field, query */) { return []; } + + getMinZoom() { + return MIN_ZOOM; + } + + getMaxZoom() { + return MAX_ZOOM; + } } diff --git a/x-pack/plugins/maps/public/layers/sources/source_registry.ts b/x-pack/plugins/maps/public/layers/sources/source_registry.ts index d16b16af74e9d..3b334d45092ad 100644 --- a/x-pack/plugins/maps/public/layers/sources/source_registry.ts +++ b/x-pack/plugins/maps/public/layers/sources/source_registry.ts @@ -10,7 +10,7 @@ import { ISource } from './source'; type SourceRegistryEntry = { ConstructorFunction: new ( sourceDescriptor: any, // this is the source-descriptor that corresponds specifically to the particular ISource instance - inspectorAdapters: unknown + inspectorAdapters?: object ) => ISource; type: string; }; diff --git a/x-pack/plugins/maps/public/layers/sources/vector_feature_types.ts b/x-pack/plugins/maps/public/layers/sources/vector_feature_types.ts new file mode 100644 index 0000000000000..9f03357e17dad --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/vector_feature_types.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. + */ + +export enum VECTOR_SHAPE_TYPES { + POINT = 'POINT', + LINE = 'LINE', + POLYGON = 'POLYGON', +} diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts index 63429830d9f4f..804915dd73052 100644 --- a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts @@ -14,6 +14,7 @@ import { VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; +import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; @@ -31,8 +32,10 @@ export interface IVectorSource extends ISource { ): Promise; getFields(): Promise; - getFieldByName(fieldName: string): IField; + getFieldByName(fieldName: string): IField | null; getSyncMeta(): VectorSourceSyncMeta; + getFieldNames(): string[]; + getApplyGlobalQuery(): boolean; } export class AbstractVectorSource extends AbstractSource implements IVectorSource { @@ -44,6 +47,21 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc ): Promise; getFields(): Promise; - getFieldByName(fieldName: string): IField; + getFieldByName(fieldName: string): IField | null; getSyncMeta(): VectorSourceSyncMeta; + getSupportedShapeTypes(): Promise; + canFormatFeatureProperties(): boolean; + getApplyGlobalQuery(): boolean; + getFieldNames(): string[]; +} + +export interface ITiledSingleLayerVectorSource extends IVectorSource { + getUrlTemplateWithMeta(): Promise<{ + layerName: string; + urlTemplate: string; + minSourceZoom: number; + maxSourceZoom: number; + }>; + getMinZoom(): number; + getMaxZoom(): number; } diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js index bb37175b48655..509584cbc415a 100644 --- a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js +++ b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js @@ -60,6 +60,10 @@ export class AbstractVectorSource extends AbstractSource { throw new Error(`Should implemement ${this.constructor.type} ${this}`); } + getFieldNames() { + return []; + } + /** * Retrieves a field. This may be an existing instance. * @param fieldName diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 6cece5efb3a5d..c46dc2cb4b73e 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -37,7 +37,7 @@ export class VectorStyleEditor extends Component { defaultDynamicProperties: getDefaultDynamicProperties(), defaultStaticProperties: getDefaultStaticProperties(), supportedFeatures: undefined, - selectedFeatureType: undefined, + selectedFeature: null, }; componentWillUnmount() { @@ -91,18 +91,20 @@ export class VectorStyleEditor extends Component { return; } - let selectedFeature = VECTOR_SHAPE_TYPES.POLYGON; - if (this.props.isPointsOnly) { - selectedFeature = VECTOR_SHAPE_TYPES.POINT; - } else if (this.props.isLinesOnly) { - selectedFeature = VECTOR_SHAPE_TYPES.LINE; + if (!_.isEqual(supportedFeatures, this.state.supportedFeatures)) { + this.setState({ supportedFeatures }); } - if ( - !_.isEqual(supportedFeatures, this.state.supportedFeatures) || - selectedFeature !== this.state.selectedFeature - ) { - this.setState({ supportedFeatures, selectedFeature }); + if (this.state.selectedFeature === null) { + let selectedFeature = VECTOR_SHAPE_TYPES.POLYGON; + if (this.props.isPointsOnly) { + selectedFeature = VECTOR_SHAPE_TYPES.POINT; + } else if (this.props.isLinesOnly) { + selectedFeature = VECTOR_SHAPE_TYPES.LINE; + } + this.setState({ + selectedFeature: selectedFeature, + }); } } diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts index 77ea44ac26bf9..e010d5ac7d7a3 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts +++ b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts @@ -7,17 +7,23 @@ import { IStyleProperty } from './properties/style_property'; import { IDynamicStyleProperty } from './properties/dynamic_style_property'; import { IVectorLayer } from '../../vector_layer'; import { IVectorSource } from '../../sources/vector_source'; -import { VectorStyleDescriptor } from '../../../../common/descriptor_types'; +import { + VectorStyleDescriptor, + VectorStylePropertiesDescriptor, +} from '../../../../common/descriptor_types'; export interface IVectorStyle { getAllStyleProperties(): IStyleProperty[]; getDescriptor(): VectorStyleDescriptor; getDynamicPropertiesArray(): IDynamicStyleProperty[]; + getSourceFieldNames(): string[]; } export class VectorStyle implements IVectorStyle { + static createDescriptor(properties: VectorStylePropertiesDescriptor): VectorStyleDescriptor; + static createDefaultStyleProperties(mapColors: string[]): VectorStylePropertiesDescriptor; constructor(descriptor: VectorStyleDescriptor, source: IVectorSource, layer: IVectorLayer); - + getSourceFieldNames(): string[]; getAllStyleProperties(): IStyleProperty[]; getDescriptor(): VectorStyleDescriptor; getDynamicPropertiesArray(): IDynamicStyleProperty[]; diff --git a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx new file mode 100644 index 0000000000000..c47cae5641e56 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiIcon } from '@elastic/eui'; +import { VectorStyle } from './styles/vector/vector_style'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants'; +import { VectorLayer, VectorLayerArguments } from './vector_layer'; +import { canSkipSourceUpdate } from './util/can_skip_fetch'; +import { ITiledSingleLayerVectorSource } from './sources/vector_source'; +import { SyncContext } from '../actions/map_actions'; +import { ISource } from './sources/source'; +import { VectorLayerDescriptor, VectorSourceRequestMeta } from '../../common/descriptor_types'; +import { MVTSingleLayerVectorSourceConfig } from './sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor'; + +export class TiledVectorLayer extends VectorLayer { + static type = LAYER_TYPE.TILED_VECTOR; + + static createDescriptor( + descriptor: VectorLayerDescriptor, + mapColors: string[] + ): VectorLayerDescriptor { + const layerDescriptor = super.createDescriptor(descriptor, mapColors); + layerDescriptor.type = TiledVectorLayer.type; + + if (!layerDescriptor.style) { + const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors); + layerDescriptor.style = VectorStyle.createDescriptor(styleProperties); + } + + return layerDescriptor; + } + + readonly _source: ITiledSingleLayerVectorSource; // downcast to the more specific type + + constructor({ layerDescriptor, source }: VectorLayerArguments) { + super({ layerDescriptor, source }); + this._source = source as ITiledSingleLayerVectorSource; + } + + getCustomIconAndTooltipContent() { + return { + icon: , + }; + } + + async _syncMVTUrlTemplate({ startLoading, stopLoading, onLoadError, dataFilters }: SyncContext) { + const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_ID_ORIGIN}`); + const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( + dataFilters, + this.getSource(), + this._style + ); + const prevDataRequest = this.getSourceDataRequest(); + + const canSkip = await canSkipSourceUpdate({ + source: this._source as ISource, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkip) { + return null; + } + + startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, searchFilters); + try { + const templateWithMeta = await this._source.getUrlTemplateWithMeta(); + stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, templateWithMeta, {}); + } catch (error) { + onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); + } + } + + async syncData(syncContext: SyncContext) { + if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { + return; + } + + await this._syncSourceStyleMeta(syncContext, this._source, this._style); + await this._syncSourceFormatters(syncContext, this._source, this._style); + await this._syncMVTUrlTemplate(syncContext); + } + + _syncSourceBindingWithMb(mbMap: unknown) { + // @ts-ignore + const mbSource = mbMap.getSource(this.getId()); + if (!mbSource) { + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + // this is possible if the layer was invisible at startup. + // the actions will not perform any data=syncing as an optimization when a layer is invisible + // when turning the layer back into visible, it's possible the url has not been resovled yet. + return; + } + + const sourceMeta: MVTSingleLayerVectorSourceConfig | null = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if (!sourceMeta) { + return; + } + + const sourceId = this.getId(); + + // @ts-ignore + mbMap.addSource(sourceId, { + type: 'vector', + tiles: [sourceMeta.urlTemplate], + minzoom: sourceMeta.minSourceZoom, + maxzoom: sourceMeta.maxSourceZoom, + }); + } + } + + _syncStylePropertiesWithMb(mbMap: unknown) { + // @ts-ignore + const mbSource = mbMap.getSource(this.getId()); + if (!mbSource) { + return; + } + + const sourceDataRequest = this.getSourceDataRequest(); + if (!sourceDataRequest) { + return; + } + const sourceMeta: MVTSingleLayerVectorSourceConfig = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig; + + this._setMbPointsProperties(mbMap, sourceMeta.layerName); + this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName); + } + + _requiresPrevSourceCleanup(mbMap: unknown): boolean { + // @ts-ignore + const mbTileSource = mbMap.getSource(this.getId()); + if (!mbTileSource) { + return false; + } + const dataRequest = this.getSourceDataRequest(); + if (!dataRequest) { + return false; + } + const tiledSourceMeta: MVTSingleLayerVectorSourceConfig | null = dataRequest.getData() as MVTSingleLayerVectorSourceConfig; + if ( + mbTileSource.tiles[0] === tiledSourceMeta.urlTemplate && + mbTileSource.minzoom === tiledSourceMeta.minSourceZoom && + mbTileSource.maxzoom === tiledSourceMeta.maxSourceZoom + ) { + // TileURL and zoom-range captures all the state. If this does not change, no updates are required. + return false; + } + + return true; + } + + syncLayerWithMB(mbMap: unknown) { + this._removeStaleMbSourcesAndLayers(mbMap); + this._syncSourceBindingWithMb(mbMap); + this._syncStylePropertiesWithMb(mbMap); + } + + getJoins() { + return []; + } + + getMinZoom() { + // higher resolution vector tiles cannot be displayed at lower-res + return Math.max(this._source.getMinZoom(), super.getMinZoom()); + } +} diff --git a/x-pack/plugins/maps/public/layers/vector_layer.d.ts b/x-pack/plugins/maps/public/layers/vector_layer.d.ts index 88b1a1ce8535e..3d5b8054ff3fd 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/layers/vector_layer.d.ts @@ -9,6 +9,7 @@ import { AbstractLayer } from './layer'; import { IVectorSource } from './sources/vector_source'; import { MapFilters, + LayerDescriptor, VectorLayerDescriptor, VectorSourceRequestMeta, } from '../../common/descriptor_types'; @@ -20,7 +21,7 @@ import { SyncContext } from '../actions/map_actions'; type VectorLayerArguments = { source: IVectorSource; - joins: IJoin[]; + joins?: IJoin[]; layerDescriptor: VectorLayerDescriptor; }; @@ -28,11 +29,12 @@ export interface IVectorLayer extends ILayer { getFields(): Promise; getStyleEditorFields(): Promise; getValidJoins(): IJoin[]; + getSource(): IVectorSource; } export class VectorLayer extends AbstractLayer implements IVectorLayer { static createDescriptor( - options: Partial, + options: Partial, mapColors?: string[] ): VectorLayerDescriptor; @@ -40,14 +42,30 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { protected readonly _style: IVectorStyle; constructor(options: VectorLayerArguments); - + getLayerTypeIconName(): string; getFields(): Promise; getStyleEditorFields(): Promise; getValidJoins(): IJoin[]; + _syncSourceStyleMeta( + syncContext: SyncContext, + source: IVectorSource, + style: IVectorStyle + ): Promise; + _syncSourceFormatters( + syncContext: SyncContext, + source: IVectorSource, + style: IVectorStyle + ): Promise; + syncLayerWithMB(mbMap: unknown): void; _getSearchFilters( dataFilters: MapFilters, source: IVectorSource, style: IVectorStyle ): VectorSourceRequestMeta; _syncData(syncContext: SyncContext, source: IVectorSource, style: IVectorStyle): Promise; + ownsMbSourceId(sourceId: string): boolean; + ownsMbLayerId(sourceId: string): boolean; + _setMbPointsProperties(mbMap: unknown, mvtSourceLayer?: string): void; + _setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void; + getSource(): IVectorSource; } diff --git a/x-pack/plugins/maps/public/layers/vector_layer.js b/x-pack/plugins/maps/public/layers/vector_layer.js index d606420909281..c5947a63587ea 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_layer.js @@ -641,7 +641,7 @@ export class VectorLayer extends AbstractLayer { } } - _setMbPointsProperties(mbMap) { + _setMbPointsProperties(mbMap, mvtSourceLayer) { const pointLayerId = this._getMbPointLayerId(); const symbolLayerId = this._getMbSymbolLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); @@ -658,7 +658,7 @@ export class VectorLayer extends AbstractLayer { if (symbolLayer) { mbMap.setLayoutProperty(symbolLayerId, 'visibility', 'none'); } - this._setMbCircleProperties(mbMap); + this._setMbCircleProperties(mbMap, mvtSourceLayer); } else { markerLayerId = symbolLayerId; textLayerId = symbolLayerId; @@ -666,7 +666,7 @@ export class VectorLayer extends AbstractLayer { mbMap.setLayoutProperty(pointLayerId, 'visibility', 'none'); mbMap.setLayoutProperty(this._getMbTextLayerId(), 'visibility', 'none'); } - this._setMbSymbolProperties(mbMap); + this._setMbSymbolProperties(mbMap, mvtSourceLayer); } this.syncVisibilityWithMb(mbMap, markerLayerId); @@ -677,27 +677,36 @@ export class VectorLayer extends AbstractLayer { } } - _setMbCircleProperties(mbMap) { + _setMbCircleProperties(mbMap, mvtSourceLayer) { const sourceId = this.getId(); const pointLayerId = this._getMbPointLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); if (!pointLayer) { - mbMap.addLayer({ + const mbLayer = { id: pointLayerId, type: 'circle', source: sourceId, paint: {}, - }); + }; + + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); } const textLayerId = this._getMbTextLayerId(); const textLayer = mbMap.getLayer(textLayerId); if (!textLayer) { - mbMap.addLayer({ + const mbLayer = { id: textLayerId, type: 'symbol', source: sourceId, - }); + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); } const filterExpr = getPointFilterExpression(this._hasJoins()); @@ -719,17 +728,21 @@ export class VectorLayer extends AbstractLayer { }); } - _setMbSymbolProperties(mbMap) { + _setMbSymbolProperties(mbMap, mvtSourceLayer) { const sourceId = this.getId(); const symbolLayerId = this._getMbSymbolLayerId(); const symbolLayer = mbMap.getLayer(symbolLayerId); if (!symbolLayer) { - mbMap.addLayer({ + const mbLayer = { id: symbolLayerId, type: 'symbol', source: sourceId, - }); + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); } const filterExpr = getPointFilterExpression(this._hasJoins()); @@ -750,26 +763,34 @@ export class VectorLayer extends AbstractLayer { }); } - _setMbLinePolygonProperties(mbMap) { + _setMbLinePolygonProperties(mbMap, mvtSourceLayer) { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); const hasJoins = this._hasJoins(); if (!mbMap.getLayer(fillLayerId)) { - mbMap.addLayer({ + const mbLayer = { id: fillLayerId, type: 'fill', source: sourceId, paint: {}, - }); + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); } if (!mbMap.getLayer(lineLayerId)) { - mbMap.addLayer({ + const mbLayer = { id: lineLayerId, type: 'line', source: sourceId, paint: {}, - }); + }; + if (mvtSourceLayer) { + mbLayer['source-layer'] = mvtSourceLayer; + } + mbMap.addLayer(mbLayer); } this.getCurrentStyle().setMBPaintProperties({ alpha: this.getAlpha(), diff --git a/x-pack/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/plugins/maps/public/layers/vector_tile_layer.js index 44987fd3e78f0..c620ec6c56dc3 100644 --- a/x-pack/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_tile_layer.js @@ -161,19 +161,7 @@ export class VectorTileLayer extends TileLayer { return; } - if (this._requiresPrevSourceCleanup(mbMap)) { - const mbStyle = mbMap.getStyle(); - mbStyle.layers.forEach(mbLayer => { - if (this.ownsMbLayerId(mbLayer.id)) { - mbMap.removeLayer(mbLayer.id); - } - }); - Object.keys(mbStyle.sources).some(mbSourceId => { - if (this.ownsMbSourceId(mbSourceId)) { - mbMap.removeSource(mbSourceId); - } - }); - } + this._removeStaleMbSourcesAndLayers(mbMap); let initialBootstrapCompleted = false; const sourceIds = Object.keys(vectorStyle.sources); diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index e5bad6e3f123c..ab81efd7c4d2e 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -35,6 +35,7 @@ import { } from './kibana_services'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { getMapsVisTypeAlias } from './maps_vis_type_alias'; +import { registerLayerWizards } from './layers/load_layer_wizards'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -74,6 +75,7 @@ export const bindStartCoreAndPlugins = (core: CoreStart, plugins: any) => { setUiActions(plugins.uiActions); setNavigation(plugins.navigation); setCoreI18n(core.i18n); + registerLayerWizards(); }; /** diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.js b/x-pack/plugins/maps/public/selectors/map_selectors.js index cd361a99f39f0..46793deee9193 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/plugins/maps/public/selectors/map_selectors.js @@ -20,6 +20,8 @@ import { BlendedVectorLayer } from '../layers/blended_vector_layer'; import { getTimeFilter } from '../kibana_services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getInspectorAdapters } from '../reducers/non_serializable_instances'; +import { TiledVectorLayer } from '../layers/tiled_vector_layer'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR, @@ -51,6 +53,8 @@ function createLayerInstance(layerDescriptor, inspectorAdapters) { return new HeatmapLayer({ layerDescriptor, source }); case BlendedVectorLayer.type: return new BlendedVectorLayer({ layerDescriptor, source }); + case TiledVectorLayer.type: + return new TiledVectorLayer({ layerDescriptor, source }); default: throw new Error(`Unrecognized layerType ${layerDescriptor.type}`); } diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/plugins/maps/public/selectors/map_selectors.test.js index b83be301653b8..72cc748617540 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.js +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.js @@ -5,6 +5,7 @@ */ jest.mock('../../../../../plugins/maps/public/layers/vector_layer', () => {}); +jest.mock('../../../../../plugins/maps/public/layers/tiled_vector_layer', () => {}); jest.mock('../../../../../plugins/maps/public/layers/blended_vector_layer', () => {}); jest.mock('../../../../../plugins/maps/public/layers/heatmap_layer', () => {}); jest.mock('../../../../../plugins/maps/public/layers/vector_tile_layer', () => {}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 511ebb7e1647a..3c959b827bb1c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -13,7 +13,6 @@ import { ml } from '../../services/ml_api_service'; import { Dictionary } from '../../../../common/types/common'; import { getErrorMessage } from '../../../../common/util/errors'; import { SavedSearchQuery } from '../../contexts/ml'; -import { SortDirection } from '../../components/ml_in_memory_table'; export type IndexName = string; export type IndexPattern = string; @@ -53,13 +52,9 @@ export interface ClassificationAnalysis { classification: Classification; } -export interface LoadExploreDataArg { - field: string; - direction: SortDirection; +export interface LoadRegressionExploreDataArg { + filterByIsTraining?: boolean; searchQuery: SavedSearchQuery; - requiresKeyword?: boolean; - pageIndex?: number; - pageSize?: number; } export const SEARCH_SIZE = 1000; @@ -272,6 +267,11 @@ export const isResultsSearchBoolQuery = (arg: any): arg is ResultsSearchBoolQuer return keys.length === 1 && keys[0] === 'bool'; }; +export const isQueryStringQuery = (arg: any): arg is QueryStringQuery => { + const keys = Object.keys(arg); + return keys.length === 1 && keys[0] === 'query_string'; +}; + export const isRegressionEvaluateResponse = (arg: any): arg is RegressionEvaluateResponse => { const keys = Object.keys(arg); return ( @@ -396,6 +396,10 @@ interface ResultsSearchTermQuery { term: Dictionary; } +interface QueryStringQuery { + query_string: Dictionary; +} + export type ResultsSearchQuery = ResultsSearchBoolQuery | ResultsSearchTermQuery | SavedSearchQuery; export function getEvalQueryBody({ @@ -409,16 +413,34 @@ export function getEvalQueryBody({ searchQuery?: ResultsSearchQuery; ignoreDefaultQuery?: boolean; }) { - let query: ResultsSearchQuery = { + let query; + + const trainingQuery: ResultsSearchQuery = { term: { [`${resultsField}.is_training`]: { value: isTraining } }, }; - if (searchQuery !== undefined && ignoreDefaultQuery === true) { - query = searchQuery; - } else if (searchQuery !== undefined && isResultsSearchBoolQuery(searchQuery)) { - const searchQueryClone = cloneDeep(searchQuery); - searchQueryClone.bool.must.push(query); + const searchQueryClone = cloneDeep(searchQuery); + + if (isResultsSearchBoolQuery(searchQueryClone)) { + if (searchQueryClone.bool.must === undefined) { + searchQueryClone.bool.must = []; + } + + searchQueryClone.bool.must.push(trainingQuery); query = searchQueryClone; + } else if (isQueryStringQuery(searchQueryClone)) { + query = { + bool: { + must: [searchQueryClone, trainingQuery], + }, + }; + } else { + // Not a bool or string query so we need to create it so can add the trainingQuery + query = { + bool: { + must: [trainingQuery], + }, + }; } return query; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index 92d8731959895..f165669bdd674 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -246,8 +246,15 @@ export const getDefaultFieldsFromJobCaps = ( fields: Field[], jobConfig: DataFrameAnalyticsConfig, needsDestIndexFields: boolean -): { selectedFields: Field[]; docFields: Field[]; depVarType?: ES_FIELD_TYPES } => { - const fieldsObj = { selectedFields: [], docFields: [] }; +): { + selectedFields: Field[]; + docFields: Field[]; + depVarType?: ES_FIELD_TYPES; +} => { + const fieldsObj = { + selectedFields: [], + docFields: [], + }; if (fields.length === 0) { return fieldsObj; } @@ -267,38 +274,37 @@ export const getDefaultFieldsFromJobCaps = ( const featureImportanceFields = []; if ((numTopFeatureImportanceValues ?? 0) > 0) { - featureImportanceFields.push( - ...fields.map(d => ({ - id: `${resultsField}.feature_importance.${d.id}`, - name: `${resultsField}.feature_importance.${d.name}`, - type: KBN_FIELD_TYPES.NUMBER, - })) - ); + featureImportanceFields.push({ + id: `${resultsField}.feature_importance`, + name: `${resultsField}.feature_importance`, + type: KBN_FIELD_TYPES.NUMBER, + }); } + let allFields: any = []; // Only need to add these fields if we didn't use dest index pattern to get the fields - const allFields: any = - needsDestIndexFields === true - ? [ - { - id: `${resultsField}.is_training`, - name: `${resultsField}.is_training`, - type: ES_FIELD_TYPES.BOOLEAN, - }, - { id: predictedField, name: predictedField, type }, - ...featureImportanceFields, - ] - : []; - - allFields.push(...fields); + if (needsDestIndexFields === true) { + allFields.push( + { + id: `${resultsField}.is_training`, + name: `${resultsField}.is_training`, + type: ES_FIELD_TYPES.BOOLEAN, + }, + { id: predictedField, name: predictedField, type } + ); + } + + allFields.push(...fields, ...featureImportanceFields); allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) => sortRegressionResultsFields(a, b, jobConfig) ); + // Remove feature_importance fields provided by dest index since feature_importance is an array the path is not valid + if (needsDestIndexFields === false) { + allFields = allFields.filter((field: any) => !field.name.includes('.feature_importance.')); + } let selectedFields = allFields.filter( - (field: any) => - field.name === predictedField || - (!field.name.includes('.keyword') && !field.name.includes('.feature_importance.')) + (field: any) => field.name === predictedField || !field.name.includes('.keyword') ); if (selectedFields.length > DEFAULT_REGRESSION_COLUMNS) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts index 6038def592e5c..9527a9adb98ce 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts @@ -22,7 +22,6 @@ import { newJobCapsService } from '../../../../../services/new_job_capabilities_ import { Field } from '../../../../../../../common/types/fields'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { - LoadExploreDataArg, defaultSearchQuery, ResultsSearchQuery, isResultsSearchBoolQuery, @@ -37,12 +36,23 @@ import { SEARCH_SIZE, SearchQuery, } from '../../../../common'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; + +interface LoadClassificationExploreDataArg { + direction: SortDirection; + filterByIsTraining?: boolean; + field: string; + searchQuery: SavedSearchQuery; + requiresKeyword?: boolean; + pageIndex?: number; + pageSize?: number; +} export type TableItem = Record; export interface UseExploreDataReturnType { errorMessage: string; - loadExploreData: (arg: LoadExploreDataArg) => void; + loadExploreData: (arg: LoadClassificationExploreDataArg) => void; sortField: EsFieldName; sortDirection: SortDirection; status: INDEX_STATUS; @@ -84,7 +94,7 @@ export const useExploreData = ( direction, searchQuery, requiresKeyword, - }: LoadExploreDataArg) => { + }: LoadClassificationExploreDataArg) => { if (jobConfig !== undefined) { setErrorMessage(''); setStatus(INDEX_STATUS.LOADING); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 9f235ae6c45c0..6ef6666be5ec6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useEffect, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -17,7 +17,7 @@ import { EuiTitle, } from '@elastic/eui'; import { useMlKibana } from '../../../../../contexts/kibana'; -import { ErrorCallout } from '../error_callout'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getValuesFromResponse, getDependentVar, @@ -33,14 +33,13 @@ import { EvaluateStat } from './evaluate_stat'; import { isResultsSearchBoolQuery, isRegressionEvaluateResponse, - ResultsSearchQuery, ANALYSIS_CONFIG_TYPE, } from '../../../../common/analytics'; interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus?: DATA_FRAME_TASK_STATE; - searchQuery: ResultsSearchQuery; + searchQuery: SavedSearchQuery; } const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; @@ -54,6 +53,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const [generalizationEval, setGeneralizationEval] = useState(defaultEval); const [isLoadingTraining, setIsLoadingTraining] = useState(false); const [isLoadingGeneralization, setIsLoadingGeneralization] = useState(false); + const [isTrainingFilter, setIsTrainingFilter] = useState(undefined); const [trainingDocsCount, setTrainingDocsCount] = useState(null); const [generalizationDocsCount, setGeneralizationDocsCount] = useState(null); @@ -92,8 +92,8 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) } else { setIsLoadingGeneralization(false); setGeneralizationEval({ - meanSquaredError: '', - rSquared: '', + meanSquaredError: '--', + rSquared: '--', error: genErrorEval.error, }); } @@ -128,108 +128,78 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) } else { setIsLoadingTraining(false); setTrainingEval({ - meanSquaredError: '', - rSquared: '', + meanSquaredError: '--', + rSquared: '--', error: trainingErrorEval.error, }); } }; - const loadData = async ({ - isTrainingClause, - }: { - isTrainingClause?: { query: string; operator: string }; - }) => { - // searchBar query is filtering for testing data - if (isTrainingClause !== undefined && isTrainingClause.query === 'false') { - loadGeneralizationData(); - - const docsCountResp = await loadDocsCount({ - isTraining: false, - searchQuery, - resultsField, - destIndex: jobConfig.dest.index, - }); - - if (docsCountResp.success === true) { - setGeneralizationDocsCount(docsCountResp.docsCount); - } else { - setGeneralizationDocsCount(null); - } - - setTrainingDocsCount(0); - setTrainingEval({ - meanSquaredError: '--', - rSquared: '--', - error: null, - }); - } else if (isTrainingClause !== undefined && isTrainingClause.query === 'true') { - // searchBar query is filtering for training data - loadTrainingData(); - - const docsCountResp = await loadDocsCount({ - isTraining: true, - searchQuery, - resultsField, - destIndex: jobConfig.dest.index, - }); - - if (docsCountResp.success === true) { - setTrainingDocsCount(docsCountResp.docsCount); - } else { - setTrainingDocsCount(null); - } - - setGeneralizationDocsCount(0); - setGeneralizationEval({ - meanSquaredError: '--', - rSquared: '--', - error: null, - }); + const loadData = async () => { + loadGeneralizationData(false); + const genDocsCountResp = await loadDocsCount({ + ignoreDefaultQuery: false, + isTraining: false, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, + }); + if (genDocsCountResp.success === true) { + setGeneralizationDocsCount(genDocsCountResp.docsCount); } else { - // No is_training clause/filter from search bar so load both - loadGeneralizationData(false); - const genDocsCountResp = await loadDocsCount({ - ignoreDefaultQuery: false, - isTraining: false, - searchQuery, - resultsField, - destIndex: jobConfig.dest.index, - }); - if (genDocsCountResp.success === true) { - setGeneralizationDocsCount(genDocsCountResp.docsCount); - } else { - setGeneralizationDocsCount(null); - } + setGeneralizationDocsCount(null); + } - loadTrainingData(false); - const trainDocsCountResp = await loadDocsCount({ - ignoreDefaultQuery: false, - isTraining: true, - searchQuery, - resultsField, - destIndex: jobConfig.dest.index, - }); - if (trainDocsCountResp.success === true) { - setTrainingDocsCount(trainDocsCountResp.docsCount); - } else { - setTrainingDocsCount(null); - } + loadTrainingData(false); + const trainDocsCountResp = await loadDocsCount({ + ignoreDefaultQuery: false, + isTraining: true, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, + }); + if (trainDocsCountResp.success === true) { + setTrainingDocsCount(trainDocsCountResp.docsCount); + } else { + setTrainingDocsCount(null); } }; useEffect(() => { - const hasIsTrainingClause = - isResultsSearchBoolQuery(searchQuery) && - searchQuery.bool.must.filter( - (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined - ); - const isTrainingClause = - hasIsTrainingClause && - hasIsTrainingClause[0] && - hasIsTrainingClause[0].match[`${resultsField}.is_training`]; + let isTraining: boolean | undefined; + const query = + isResultsSearchBoolQuery(searchQuery) && (searchQuery.bool.should || searchQuery.bool.filter); + + if (query !== undefined && query !== false) { + for (let i = 0; i < query.length; i++) { + const clause = query[i]; - loadData({ isTrainingClause }); + if (clause.match && clause.match[`${resultsField}.is_training`] !== undefined) { + isTraining = clause.match[`${resultsField}.is_training`]; + break; + } else if ( + clause.bool && + (clause.bool.should !== undefined || clause.bool.filter !== undefined) + ) { + const innerQuery = clause.bool.should || clause.bool.filter; + if (innerQuery !== undefined) { + for (let j = 0; j < innerQuery.length; j++) { + const innerClause = innerQuery[j]; + if ( + innerClause.match && + innerClause.match[`${resultsField}.is_training`] !== undefined + ) { + isTraining = innerClause.match[`${resultsField}.is_training`]; + break; + } + } + } + } + } + } + + setIsTrainingFilter(isTraining); + loadData(); }, [JSON.stringify(searchQuery)]); return ( @@ -293,13 +263,18 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) defaultMessage="{docsCount, plural, one {# doc} other {# docs}} evaluated" values={{ docsCount: generalizationDocsCount }} /> + {isTrainingFilter === true && generalizationDocsCount === 0 && ( + + )} )} - - {generalizationEval.error !== null && } - {generalizationEval.error === null && ( - + + + = ({ jobConfig, jobStatus, searchQuery }) isMSE={false} /> - + + + {generalizationEval.error !== null && ( + + + {generalizationEval.error} + + )}
@@ -338,13 +320,18 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) defaultMessage="{docsCount, plural, one {# doc} other {# docs}} evaluated" values={{ docsCount: trainingDocsCount }} /> + {isTrainingFilter === false && trainingDocsCount === 0 && ( + + )} )} - - {trainingEval.error !== null && } - {trainingEval.error === null && ( - + + + = ({ jobConfig, jobStatus, searchQuery }) isMSE={false} /> - + + + {trainingEval.error !== null && ( + + + {trainingEval.error} + + )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx new file mode 100644 index 0000000000000..0fcb1ed600719 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx @@ -0,0 +1,135 @@ +/* + * 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, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; + +import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; + +import { mlFieldFormatService } from '../../../../../services/field_format_service'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + +type Pagination = Pick; +type TableItem = Record; + +interface ExplorationDataGridProps { + colorRange?: (d: number) => string; + columns: any[]; + indexPattern: IndexPattern; + pagination: Pagination; + resultsField: string; + rowCount: number; + selectedFields: string[]; + setPagination: Dispatch>; + setSelectedFields: Dispatch>; + setSortingColumns: Dispatch>; + sortingColumns: EuiDataGridSorting['columns']; + tableItems: TableItem[]; +} + +export const RegressionExplorationDataGrid: FC = ({ + columns, + indexPattern, + pagination, + resultsField, + rowCount, + selectedFields, + setPagination, + setSelectedFields, + setSortingColumns, + sortingColumns, + tableItems, +}) => { + const renderCellValue = useMemo(() => { + return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const fullItem = tableItems[adjustedRowIndex]; + + if (fullItem === undefined) { + return null; + } + + let format: any; + + if (indexPattern !== undefined) { + format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); + } + + const cellValue = + fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined + ? fullItem[columnId] + : null; + + if (format !== undefined) { + return format.convert(cellValue, 'text'); + } + + if (typeof cellValue === 'string' || cellValue === null) { + return cellValue; + } + + if (typeof cellValue === 'boolean') { + return cellValue ? 'true' : 'false'; + } + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + return cellValue; + }; + }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); + + const onChangeItemsPerPage = useCallback( + pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, + [setPagination] + ); + + const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ + setPagination, + ]); + + const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); + + return ( + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index a35be5400f46b..43fa50b2e4df5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -4,72 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useEffect, useState } from 'react'; -import moment from 'moment-timezone'; +import React, { Fragment, FC, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiBadge, - EuiButtonIcon, EuiCallOut, - EuiCheckbox, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiPanel, - EuiPopover, - EuiPopoverTitle, EuiProgress, EuiSpacer, EuiText, - EuiToolTip, - Query, } from '@elastic/eui'; -import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { - ColumnType, - mlInMemoryTableBasicFactory, - OnTableChangeArg, - SortingPropType, - SORT_DIRECTION, -} from '../../../../../components/ml_in_memory_table'; - -import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; -import { Field } from '../../../../../../../common/types/fields'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES, - toggleSelectedField, - isKeywordAndTextType, sortRegressionResultsFields, } from '../../../../common/fields'; import { DataFrameAnalyticsConfig, - EsFieldName, - EsDoc, MAX_COLUMNS, - getPredictedFieldName, INDEX_STATUS, SEARCH_SIZE, defaultSearchQuery, - getDependentVar, } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { useExploreData, TableItem } from './use_explore_data'; +import { useExploreData } from './use_explore_data'; import { ExplorationTitle } from './regression_exploration'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); +import { RegressionExplorationDataGrid } from './regression_exploration_data_grid'; +import { ExplorationQueryBar } from '../exploration_query_bar'; const showingDocs = i18n.translate( 'xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText', @@ -95,308 +64,65 @@ interface Props { export const ResultsTable: FC = React.memo( ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(25); - const [selectedFields, setSelectedFields] = useState([] as Field[]); - const [docFields, setDocFields] = useState([] as Field[]); - const [depVarType, setDepVarType] = useState(undefined); - const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [searchError, setSearchError] = useState(undefined); - const [searchString, setSearchString] = useState(undefined); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - - const dependentVariable = getDependentVar(jobConfig.analysis); - - function toggleColumnsPopover() { - setColumnsPopoverVisible(!isColumnsPopoverVisible); - } - - function closeColumnsPopover() { - setColumnsPopoverVisible(false); - } - - function toggleColumn(column: EsFieldName) { - if (tableItems.length > 0 && jobConfig !== undefined) { - // spread to a new array otherwise the component wouldn't re-render - setSelectedFields([ - ...toggleSelectedField(selectedFields, column, jobConfig.dest.results_field, depVarType), - ]); - } - } - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - + const resultsField = jobConfig.dest.results_field; const { errorMessage, - loadExploreData, - sortField, - sortDirection, - status, - tableItems, - } = useExploreData( - jobConfig, - needsDestIndexFields, + fieldTypes, + pagination, + searchQuery, selectedFields, + rowCount, + setPagination, + setSearchQuery, setSelectedFields, - setDocFields, - setDepVarType - ); - - const columns: Array> = selectedFields - .sort(({ name: a }, { name: b }) => sortRegressionResultsFields(a, b, jobConfig)) - .map(field => { - const { type } = field; - let format: any; + setSortingColumns, + sortingColumns, + status, + tableFields, + tableItems, + } = useExploreData(jobConfig, needsDestIndexFields); - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, field.id, ''); - } + useEffect(() => { + setEvaluateSearchQuery(searchQuery); + }, [JSON.stringify(searchQuery)]); + const columns = tableFields + .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) + .map((field: any) => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + let isSortable = true; + const type = fieldTypes[field]; const isNumber = type !== undefined && (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - const column: ColumnType = { - field: field.name, - name: field.name, - sortable: true, - truncateText: true, - }; - - const render = (d: any, fullItem: EsDoc) => { - if (format !== undefined) { - d = format.convert(d, 'text'); - return d; - } - - if (Array.isArray(d) && d.every(item => typeof item === 'string')) { - // If the cells data is an array of strings, return as a comma separated list. - // The list will get limited to 5 items with `…` at the end if there's more in the original array. - return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`; - } else if (Array.isArray(d)) { - // If the cells data is an array of e.g. objects, display a 'array' badge with a - // tooltip that explains that this type of field is not supported in this table. - return ( - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent', - { - defaultMessage: 'array', - } - )} - - - ); - } - - return d; - }; - if (isNumber) { - column.dataType = 'number'; - column.render = render; - } else if (typeof type !== 'undefined') { - switch (type) { - case ES_FIELD_TYPES.BOOLEAN: - column.dataType = ES_FIELD_TYPES.BOOLEAN; - column.render = d => (d ? 'true' : 'false'); - break; - case ES_FIELD_TYPES.DATE: - column.align = 'right'; - if (format !== undefined) { - column.render = render; - } else { - column.render = (d: any) => { - if (d !== undefined) { - return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); - } - return d; - }; - } - break; - default: - column.render = render; - break; - } - } else { - column.render = render; + schema = 'numeric'; } - return column; - }); - - const docFieldsCount = docFields.length; - - useEffect(() => { - if ( - jobConfig !== undefined && - columns.length > 0 && - selectedFields.length > 0 && - sortField !== undefined && - sortDirection !== undefined && - selectedFields.some(field => field.name === sortField) - ) { - let field = sortField; - // If sorting by predictedField use dependentVar type - if (predictedFieldName === sortField) { - field = dependentVariable; + switch (type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'boolean': + schema = 'boolean'; + break; } - const requiresKeyword = isKeywordAndTextType(field); - - loadExploreData({ - field: sortField, - direction: sortDirection, - searchQuery, - requiresKeyword, - }); - } - }, [JSON.stringify(searchQuery)]); - - useEffect(() => { - // By default set sorting to descending on the prediction field (`_prediction`). - // if that's not available sort ascending on the first column. Check if the current sorting field is still available. - if ( - jobConfig !== undefined && - columns.length > 0 && - selectedFields.length > 0 && - !selectedFields.some(field => field.name === sortField) - ) { - const predictedFieldSelected = selectedFields.some( - field => field.name === predictedFieldName - ); - - // CHECK IF keyword suffix is needed (if predicted field is selected we have to check the dependent variable type) - let sortByField = predictedFieldSelected ? dependentVariable : selectedFields[0].name; - - const requiresKeyword = isKeywordAndTextType(sortByField); - - sortByField = predictedFieldSelected ? predictedFieldName : sortByField; - - const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field: sortByField, direction, searchQuery, requiresKeyword }); - } - }, [ - jobConfig, - columns.length, - selectedFields.length, - sortField, - sortDirection, - tableItems.length, - ]); - - let sorting: SortingPropType = false; - let onTableChange; - - if (columns.length > 0 && sortField !== '' && sortField !== undefined) { - sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - onTableChange = ({ - page = { index: 0, size: 10 }, - sort = { field: sortField, direction: sortDirection }, - }: OnTableChangeArg) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - - if (sort.field !== sortField || sort.direction !== sortDirection) { - let field = sort.field; - // If sorting by predictedField use depVar for type check - if (predictedFieldName === sort.field) { - field = dependentVariable; - } - loadExploreData({ - ...sort, - searchQuery, - requiresKeyword: isKeywordAndTextType(field), - }); + if (field === `${resultsField}.feature_importance`) { + isSortable = false; } - }; - } - - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: tableItems.length, - pageSizeOptions: PAGE_SIZE_OPTIONS, - hidePerPageOptions: false, - }; - const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { - if (error) { - setSearchError(error.message); - } else { - try { - const esQueryDsl = Query.toESQuery(query); - setSearchQuery(esQueryDsl); - setSearchString(query.text); - setSearchError(undefined); - // set query for use in evaluate panel - setEvaluateSearchQuery(esQueryDsl); - } catch (e) { - setSearchError(e.toString()); - } - } - }; + return { id: field, schema, isSortable }; + }); - const search = { - onChange: onQueryChange, - defaultQuery: searchString, - box: { - incremental: false, - placeholder: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder', - { - defaultMessage: 'E.g. avg>0.5', - } - ), - }, - filters: [ - { - type: 'field_value_toggle_group', - field: `${jobConfig.dest.results_field}.is_training`, - items: [ - { - value: false, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel', - { - defaultMessage: 'Testing', - } - ), - }, - { - value: true, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel', - { - defaultMessage: 'Training', - } - ), - }, - ], - }, - ], - }; + const docFieldsCount = tableFields.length; if (jobConfig === undefined) { return null; @@ -428,11 +154,6 @@ export const ResultsTable: FC = React.memo( ); } - const tableError = - status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') - ? errorMessage - : searchError; - return ( @@ -464,52 +185,6 @@ export const ResultsTable: FC = React.memo( )} - - - - } - isOpen={isColumnsPopoverVisible} - closePopover={closeColumnsPopover} - ownFocus - > - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle', - { - defaultMessage: 'Select fields', - } - )} - -
- {docFields.map(({ name }) => ( - field.name === name)} - onChange={() => toggleColumn(name)} - disabled={ - selectedFields.some(field => field.name === name) && - selectedFields.length === 1 - } - /> - ))} -
-
-
-
@@ -518,29 +193,39 @@ export const ResultsTable: FC = React.memo( )} {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - - - - - - - - + + + + + + + + + + + + + + + + )}
); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts index e158e952c1c18..c68fe5b2cbee8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts @@ -4,97 +4,156 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState, Dispatch, SetStateAction } from 'react'; +import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; import { SearchResponse } from 'elasticsearch'; import { cloneDeep } from 'lodash'; -import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; - import { ml } from '../../../../../services/ml_api_service'; import { getNestedProperty } from '../../../../../util/object_utils'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getDefaultFieldsFromJobCaps, + getDependentVar, getFlattenedFields, + getPredictedFieldName, DataFrameAnalyticsConfig, EsFieldName, INDEX_STATUS, - SEARCH_SIZE, - SearchQuery, } from '../../../../common'; -import { Field } from '../../../../../../../common/types/fields'; +import { Dictionary } from '../../../../../../../common/types/common'; +import { isKeywordAndTextType } from '../../../../common/fields'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { - LoadExploreDataArg, + LoadRegressionExploreDataArg, defaultSearchQuery, ResultsSearchQuery, isResultsSearchBoolQuery, } from '../../../../common/analytics'; export type TableItem = Record; +type Pagination = Pick; export interface UseExploreDataReturnType { errorMessage: string; - loadExploreData: (arg: LoadExploreDataArg) => void; - sortField: EsFieldName; - sortDirection: SortDirection; + fieldTypes: { [key: string]: ES_FIELD_TYPES }; + pagination: Pagination; + rowCount: number; + searchQuery: SavedSearchQuery; + selectedFields: EsFieldName[]; + setFilterByIsTraining: Dispatch>; + setPagination: Dispatch>; + setSearchQuery: Dispatch>; + setSelectedFields: Dispatch>; + setSortingColumns: Dispatch>; + sortingColumns: EuiDataGridSorting['columns']; status: INDEX_STATUS; + tableFields: string[]; tableItems: TableItem[]; } +type EsSorting = Dictionary<{ + order: 'asc' | 'desc'; +}>; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +interface SearchResponse7 extends SearchResponse { + hits: SearchResponse['hits'] & { + total: { + value: number; + relation: string; + }; + }; +} + export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig | undefined, - needsDestIndexFields: boolean, - selectedFields: Field[], - setSelectedFields: React.Dispatch>, - setDocFields: React.Dispatch>, - setDepVarType: React.Dispatch> + jobConfig: DataFrameAnalyticsConfig, + needsDestIndexFields: boolean ): UseExploreDataReturnType => { const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + + const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); + const [tableFields, setTableFields] = useState([]); const [tableItems, setTableItems] = useState([]); - const [sortField, setSortField] = useState(''); - const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); + const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); + const [rowCount, setRowCount] = useState(0); + + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [filterByIsTraining, setFilterByIsTraining] = useState(undefined); + const [sortingColumns, setSortingColumns] = useState([]); + + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const dependentVariable = getDependentVar(jobConfig.analysis); const getDefaultSelectedFields = () => { const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { - selectedFields: defaultSelected, - docFields, - depVarType, - } = getDefaultFieldsFromJobCaps(fields, jobConfig, needsDestIndexFields); - - setDepVarType(depVarType); - setSelectedFields(defaultSelected); - setDocFields(docFields); + const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( + fields, + jobConfig, + needsDestIndexFields + ); + + const types: { [key: string]: ES_FIELD_TYPES } = {}; + const allFields: string[] = []; + + docFields.forEach(field => { + types[field.id] = field.type; + allFields.push(field.id); + }); + + setFieldTypes(types); + setSelectedFields(defaultSelected.map(field => field.id)); + setTableFields(allFields); } }; const loadExploreData = async ({ - field, - direction, - searchQuery, - requiresKeyword, - }: LoadExploreDataArg) => { + filterByIsTraining: isTraining, + searchQuery: incomingQuery, + }: LoadRegressionExploreDataArg) => { if (jobConfig !== undefined) { setErrorMessage(''); setStatus(INDEX_STATUS.LOADING); try { const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(searchQuery); + const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); let query: ResultsSearchQuery; + const { pageIndex, pageSize } = pagination; + // If filterByIsTraining is defined - add that in to the final query + const trainingQuery = + isTraining !== undefined + ? { + term: { [`${resultsField}.is_training`]: { value: isTraining } }, + } + : undefined; - if (JSON.stringify(searchQuery) === JSON.stringify(defaultSearchQuery)) { - query = { + if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { + const existsQuery = { exists: { field: resultsField, }, }; + + query = { + bool: { + must: [existsQuery], + }, + }; + + if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { + query.bool.must.push(trainingQuery); + } } else if (isResultsSearchBoolQuery(searchQueryClone)) { if (searchQueryClone.bool.must === undefined) { searchQueryClone.bool.must = []; @@ -106,32 +165,37 @@ export const useExploreData = ( }, }); + if (trainingQuery !== undefined) { + searchQueryClone.bool.must.push(trainingQuery); + } + query = searchQueryClone; } else { query = searchQueryClone; } - const body: SearchQuery = { - query, - }; - - if (field !== undefined) { - body.sort = [ - { - [`${field}${requiresKeyword ? '.keyword' : ''}`]: { - order: direction, - }, - }, - ]; - } - const resp: SearchResponse = await ml.esSearch({ + const sort: EsSorting = sortingColumns + .map(column => { + const { id } = column; + column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; + return column; + }) + .reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const resp: SearchResponse7 = await ml.esSearch({ index: jobConfig.dest.index, - size: SEARCH_SIZE, - body, + body: { + query, + from: pageIndex * pageSize, + size: pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, }); - setSortField(field); - setSortDirection(direction); + setRowCount(resp.hits.total.value); const docs = resp.hits.hits; @@ -183,10 +247,45 @@ export const useExploreData = ( }; useEffect(() => { - if (jobConfig !== undefined) { - getDefaultSelectedFields(); - } + getDefaultSelectedFields(); }, [jobConfig && jobConfig.id]); - return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems }; + // By default set sorting to descending on the prediction field (`_prediction`). + useEffect(() => { + const sortByField = isKeywordAndTextType(dependentVariable) + ? `${predictedFieldName}.keyword` + : predictedFieldName; + const direction = SORT_DIRECTION.DESC; + + setSortingColumns([{ id: sortByField, direction }]); + }, [jobConfig && jobConfig.id]); + + useEffect(() => { + loadExploreData({ filterByIsTraining, searchQuery }); + }, [ + filterByIsTraining, + jobConfig && jobConfig.id, + pagination, + searchQuery, + selectedFields, + sortingColumns, + ]); + + return { + errorMessage, + fieldTypes, + pagination, + searchQuery, + selectedFields, + rowCount, + setFilterByIsTraining, + setPagination, + setSelectedFields, + setSortingColumns, + setSearchQuery, + sortingColumns, + status, + tableItems, + tableFields, + }; }; diff --git a/x-pack/plugins/ml/server/lib/telemetry/mappings.ts b/x-pack/plugins/ml/server/lib/telemetry/mappings.ts index 87e2243328422..5aaf9f8c79dc0 100644 --- a/x-pack/plugins/ml/server/lib/telemetry/mappings.ts +++ b/x-pack/plugins/ml/server/lib/telemetry/mappings.ts @@ -10,7 +10,7 @@ import { TELEMETRY_DOC_ID } from './telemetry'; export const mlTelemetryMappingsType: SavedObjectsType = { name: TELEMETRY_DOC_ID, hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: { file_data_visualizer: { diff --git a/x-pack/plugins/monitoring/server/lib/errors/known_errors.js b/x-pack/plugins/monitoring/server/lib/errors/known_errors.js index 6f02d0b0b26c0..17bcdd0414adf 100644 --- a/x-pack/plugins/monitoring/server/lib/errors/known_errors.js +++ b/x-pack/plugins/monitoring/server/lib/errors/known_errors.js @@ -47,5 +47,7 @@ export function isKnownError(err) { export function handleKnownError(err) { err.message = err.message + ': ' + (err.description || mapTypeMessage[err.constructor.name]); - return boomify(err, { statusCode: KNOWN_ERROR_STATUS_CODE }); + let statusCode = err.statusCode || err.status; + statusCode = statusCode !== 500 ? statusCode : KNOWN_ERROR_STATUS_CODE; + return boomify(err, { statusCode }); } diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 0fa1a5bf144ac..a45e80ac71d65 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -10,12 +10,6 @@ import { i18n } from '@kbn/i18n'; import { has, get } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; -import { - LOGGING_TAG, - KIBANA_MONITORING_LOGGING_TAG, - KIBANA_ALERTING_ENABLED, - KIBANA_STATS_TYPE_MONITORING, -} from '../common/constants'; import { Logger, PluginInitializerContext, @@ -27,7 +21,15 @@ import { CoreStart, IRouter, IClusterClient, -} from '../../../../src/core/server'; + CustomHttpResponseOptions, + ResponseError, +} from 'kibana/server'; +import { + LOGGING_TAG, + KIBANA_MONITORING_LOGGING_TAG, + KIBANA_ALERTING_ENABLED, + KIBANA_STATS_TYPE_MONITORING, +} from '../common/constants'; import { MonitoringConfig } from './config'; // @ts-ignore import { requireUIRoutes } from './routes'; @@ -92,6 +94,16 @@ interface IBulkUploader { // This is used to test the version of kibana const snapshotRegex = /-snapshot/i; +const wrapError = (error: any): CustomHttpResponseOptions => { + const options = { statusCode: error.statusCode ?? 500 }; + const boom = Boom.isBoom(error) ? error : Boom.boomify(error, options); + return { + body: boom, + headers: boom.output.headers, + statusCode: boom.output.statusCode, + }; +}; + export class Plugin { private readonly initializerContext: PluginInitializerContext; private readonly log: Logger; @@ -369,12 +381,16 @@ export class Plugin { }, }, }; - - const result = await options.handler(legacyRequest); - if (Boom.isBoom(result)) { - return res.customError({ statusCode: result.output.statusCode, body: result }); + try { + const result = await options.handler(legacyRequest); + return res.ok({ body: result }); + } catch (err) { + const statusCode: number = err.output?.statusCode || err.statusCode || err.status; + if (Boom.isBoom(err) || statusCode !== 500) { + return res.customError({ statusCode, body: err }); + } + return res.internalError(wrapError(err)); } - return res.ok({ body: result }); }; const validate: any = get(options, 'config.validate', false); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js index 8d6fe04cdb7bd..240cb84539dbf 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js @@ -46,9 +46,13 @@ export function clusterRoute(server) { codePaths: req.payload.codePaths, }; - return getClustersFromRequest(req, indexPatterns, options).catch(err => - handleError(err, req) - ); + let clusters = []; + try { + clusters = await getClustersFromRequest(req, indexPatterns, options); + } catch (err) { + throw handleError(err, req); + } + return clusters; }, }); } diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 68acf68f46109..9dd4aaafa3494 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -157,12 +157,12 @@ export class Plugin { defineRoutes({ router: core.http.createRouter(), basePath: core.http.basePath, + httpResources: core.http.resources, logger: this.initializerContext.logger.get('routes'), clusterClient: this.clusterClient, config, authc, authz, - csp: core.http.csp, license, }); diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 7e9eb75bbf753..d09f65525f44e 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -11,17 +11,6 @@ import { defineCommonRoutes } from './common'; import { defineOIDCRoutes } from './oidc'; import { RouteDefinitionParams } from '..'; -export function createCustomResourceResponse(body: string, contentType: string, cspHeader: string) { - return { - body, - headers: { - 'content-type': contentType, - 'content-security-policy': cspHeader, - }, - statusCode: 200, - }; -} - export function defineAuthenticationRoutes(params: RouteDefinitionParams) { defineSessionRoutes(params); defineCommonRoutes(params); diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts index d325a453af9d1..5d8a7ae7bdfea 100644 --- a/x-pack/plugins/security/server/routes/authentication/oidc.ts +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -8,7 +8,6 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { KibanaRequest, KibanaResponseFactory } from '../../../../../../src/core/server'; import { OIDCLogin } from '../../authentication'; -import { createCustomResourceResponse } from '.'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../errors'; import { @@ -20,7 +19,13 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for SAML authentication. */ -export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: RouteDefinitionParams) { +export function defineOIDCRoutes({ + router, + httpResources, + logger, + authc, + basePath, +}: RouteDefinitionParams) { // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. for (const path of ['/api/security/oidc/implicit', '/api/security/v1/oidc/implicit']) { /** @@ -28,7 +33,7 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route * is used, so that we can extract authentication response from URL fragment and send it to * the `/api/security/oidc/callback` route. */ - router.get( + httpResources.register( { path, validate: false, @@ -42,18 +47,14 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route { tags: ['deprecation'] } ); } - return response.custom( - createCustomResourceResponse( - ` - - Kibana OpenID Connect Login - - - `, - 'text/html', - csp.header - ) - ); + return response.renderHtml({ + body: ` + + Kibana OpenID Connect Login + + + `, + }); } ); } @@ -63,7 +64,7 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route * that extracts fragment part from the URL and send it to the `/api/security/oidc/callback` route. * We need this separate endpoint because of default CSP policy that forbids inline scripts. */ - router.get( + httpResources.register( { path: '/internal/security/oidc/implicit.js', validate: false, @@ -71,17 +72,13 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route }, (context, request, response) => { const serverBasePath = basePath.serverBasePath; - return response.custom( - createCustomResourceResponse( - ` + return response.renderJs({ + body: ` window.location.replace( '${serverBasePath}/api/security/oidc/callback?authenticationResponseURI=' + encodeURIComponent(window.location.href) ); `, - 'text/javascript', - csp.header - ) - ); + }); } ); @@ -155,7 +152,9 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route } if (!loginAttempt) { - return response.badRequest({ body: 'Unrecognized login attempt.' }); + return response.badRequest({ + body: 'Unrecognized login attempt.', + }); } return performOIDCLogin(request, response, loginAttempt); diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 8f08f250a1c75..30e1f6f336bdd 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -7,14 +7,19 @@ import { schema } from '@kbn/config-schema'; import { SAMLLogin } from '../../authentication'; import { SAMLAuthenticationProvider } from '../../authentication/providers'; -import { createCustomResourceResponse } from '.'; import { RouteDefinitionParams } from '..'; /** * Defines routes required for SAML authentication. */ -export function defineSAMLRoutes({ router, logger, authc, csp, basePath }: RouteDefinitionParams) { - router.get( +export function defineSAMLRoutes({ + router, + httpResources, + logger, + authc, + basePath, +}: RouteDefinitionParams) { + httpResources.register( { path: '/internal/security/saml/capture-url-fragment', validate: false, @@ -22,39 +27,30 @@ export function defineSAMLRoutes({ router, logger, authc, csp, basePath }: Route }, (context, request, response) => { // We're also preventing `favicon.ico` request since it can cause new SAML handshake. - return response.custom( - createCustomResourceResponse( - ` + return response.renderHtml({ + body: ` Kibana SAML Login `, - 'text/html', - csp.header - ) - ); + }); } ); - - router.get( + httpResources.register( { path: '/internal/security/saml/capture-url-fragment.js', validate: false, options: { authRequired: false }, }, (context, request, response) => { - return response.custom( - createCustomResourceResponse( - ` + return response.renderJs({ + body: ` window.location.replace( '${basePath.serverBasePath}/internal/security/saml/start?redirectURLFragment=' + encodeURIComponent(window.location.hash) ); `, - 'text/javascript', - csp.header - ) - ); + }); } ); diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index aaefdad6c221a..b0c74b98ee19b 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -8,6 +8,7 @@ import { elasticsearchServiceMock, httpServiceMock, loggingServiceMock, + httpResourcesMock, } from '../../../../../src/core/server/mocks'; import { authenticationMock } from '../authentication/index.mock'; import { authorizationMock } from '../authorization/index.mock'; @@ -27,5 +28,6 @@ export const routeDefinitionParamsMock = { authc: authenticationMock.create(), authz: authorizationMock.create(), license: licenseMock.create(), + httpResources: httpResourcesMock.createRegistrar(), }), }; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index a372fcf092707..e43072b95c906 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, IClusterClient, IRouter, Logger } from '../../../../../src/core/server'; +import { + CoreSetup, + HttpResources, + IClusterClient, + IRouter, + Logger, +} from '../../../../../src/core/server'; import { SecurityLicense } from '../../common/licensing'; import { Authentication } from '../authentication'; import { Authorization } from '../authorization'; @@ -24,7 +30,7 @@ import { defineViewRoutes } from './views'; export interface RouteDefinitionParams { router: IRouter; basePath: CoreSetup['http']['basePath']; - csp: CoreSetup['http']['csp']; + httpResources: HttpResources; logger: Logger; clusterClient: IClusterClient; config: ConfigType; diff --git a/x-pack/plugins/security/server/routes/views/account_management.ts b/x-pack/plugins/security/server/routes/views/account_management.ts index 3c84483d8f494..696a5e12b64c1 100644 --- a/x-pack/plugins/security/server/routes/views/account_management.ts +++ b/x-pack/plugins/security/server/routes/views/account_management.ts @@ -9,11 +9,8 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for the Account Management view. */ -export function defineAccountManagementRoutes({ router, csp }: RouteDefinitionParams) { - router.get({ path: '/security/account', validate: false }, async (context, request, response) => { - return response.ok({ - body: await context.core.rendering.render({ includeUserSettings: true }), - headers: { 'content-security-policy': csp.header }, - }); - }); +export function defineAccountManagementRoutes({ httpResources }: RouteDefinitionParams) { + httpResources.register({ path: '/security/account', validate: false }, (context, req, res) => + res.renderCoreApp() + ); } diff --git a/x-pack/plugins/security/server/routes/views/index.test.ts b/x-pack/plugins/security/server/routes/views/index.test.ts index 80f7f62a5ff43..a8e7e905b119a 100644 --- a/x-pack/plugins/security/server/routes/views/index.test.ts +++ b/x-pack/plugins/security/server/routes/views/index.test.ts @@ -17,7 +17,8 @@ describe('View routes', () => { defineViewRoutes(routeParamsMock); - expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path)) + .toMatchInlineSnapshot(` Array [ "/security/account", "/security/logged_out", @@ -25,6 +26,9 @@ describe('View routes', () => { "/security/overwritten_session", ] `); + expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot( + `Array []` + ); }); it('registers Login routes if `basic` provider is enabled', () => { @@ -35,16 +39,21 @@ describe('View routes', () => { defineViewRoutes(routeParamsMock); - expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path)) + .toMatchInlineSnapshot(` Array [ "/login", - "/internal/security/login_state", "/security/account", "/security/logged_out", "/logout", "/security/overwritten_session", ] `); + expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + Array [ + "/internal/security/login_state", + ] + `); }); it('registers Login routes if `token` provider is enabled', () => { @@ -55,16 +64,21 @@ describe('View routes', () => { defineViewRoutes(routeParamsMock); - expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path)) + .toMatchInlineSnapshot(` Array [ "/login", - "/internal/security/login_state", "/security/account", "/security/logged_out", "/logout", "/security/overwritten_session", ] `); + expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + Array [ + "/internal/security/login_state", + ] + `); }); it('registers Login routes if Login Selector is enabled even if both `token` and `basic` providers are not enabled', () => { @@ -75,15 +89,20 @@ describe('View routes', () => { defineViewRoutes(routeParamsMock); - expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path)) + .toMatchInlineSnapshot(` Array [ "/login", - "/internal/security/login_state", "/security/account", "/security/logged_out", "/logout", "/security/overwritten_session", ] `); + expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + Array [ + "/internal/security/login_state", + ] + `); }); }); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.test.ts b/x-pack/plugins/security/server/routes/views/logged_out.test.ts index 822802b62d874..3ff05d242d9dd 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.test.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.test.ts @@ -4,20 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - RequestHandler, - RouteConfig, - kibanaResponseFactory, -} from '../../../../../../src/core/server'; +import { HttpResourcesRequestHandler, RouteConfig } from '../../../../../../src/core/server'; import { Authentication } from '../../authentication'; import { defineLoggedOutRoutes } from './logged_out'; -import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { httpServerMock, httpResourcesMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; describe('LoggedOut view routes', () => { let authc: jest.Mocked; - let routeHandler: RequestHandler; + let routeHandler: HttpResourcesRequestHandler; let routeConfig: RouteConfig; beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); @@ -28,7 +24,7 @@ describe('LoggedOut view routes', () => { const [ loggedOutRouteConfig, loggedOutRouteHandler, - ] = routeParamsMock.router.get.mock.calls.find( + ] = routeParamsMock.httpResources.register.mock.calls.find( ([{ path }]) => path === '/security/logged_out' )!; @@ -51,9 +47,11 @@ describe('LoggedOut view routes', () => { const request = httpServerMock.createKibanaRequest(); - await expect(routeHandler({} as any, request, kibanaResponseFactory)).resolves.toEqual({ - options: { headers: { location: '/mock-server-basepath/' } }, - status: 302, + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler({} as any, request, responseFactory); + + expect(responseFactory.redirected).toHaveBeenCalledWith({ + headers: { location: '/mock-server-basepath/' }, }); expect(authc.getSessionInfo).toHaveBeenCalledWith(request); @@ -63,21 +61,10 @@ describe('LoggedOut view routes', () => { authc.getSessionInfo.mockResolvedValue(null); const request = httpServerMock.createKibanaRequest(); - const contextMock = coreMock.createRequestHandlerContext(); - - await expect( - routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) - ).resolves.toEqual({ - options: { - headers: { - 'content-security-policy': - "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - }, - }, - status: 200, - }); + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler({} as any, request, responseFactory); expect(authc.getSessionInfo).toHaveBeenCalledWith(request); - expect(contextMock.rendering.render).toHaveBeenCalledWith({ includeUserSettings: false }); + expect(responseFactory.renderAnonymousCoreApp).toHaveBeenCalledWith(); }); }); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.ts b/x-pack/plugins/security/server/routes/views/logged_out.ts index 2f69d8c35f03e..43c2f01b1b53d 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.ts @@ -16,13 +16,12 @@ import { RouteDefinitionParams } from '..'; * Defines routes required for the Logged Out view. */ export function defineLoggedOutRoutes({ - router, logger, authc, - csp, + httpResources, basePath, }: RouteDefinitionParams) { - router.get( + httpResources.register( { path: '/security/logged_out', validate: false, @@ -39,10 +38,7 @@ export function defineLoggedOutRoutes({ }); } - return response.ok({ - body: await context.core.rendering.render({ includeUserSettings: false }), - headers: { 'content-security-policy': csp.header }, - }); + return response.renderAnonymousCoreApp(); } ); } diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts index 7751f9a952c09..d43319efbdfb9 100644 --- a/x-pack/plugins/security/server/routes/views/login.test.ts +++ b/x-pack/plugins/security/server/routes/views/login.test.ts @@ -7,26 +7,34 @@ import { URL } from 'url'; import { Type } from '@kbn/config-schema'; import { + HttpResources, + HttpResourcesRequestHandler, + IRouter, RequestHandler, - RouteConfig, kibanaResponseFactory, - IRouter, + RouteConfig, } from '../../../../../../src/core/server'; import { SecurityLicense } from '../../../common/licensing'; import { LoginState } from '../../../common/login_state'; import { ConfigType } from '../../config'; import { defineLoginRoutes } from './login'; -import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { + coreMock, + httpServerMock, + httpResourcesMock, +} from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; describe('Login view routes', () => { + let httpResources: jest.Mocked; let router: jest.Mocked; let license: jest.Mocked; let config: ConfigType; beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); router = routeParamsMock.router; + httpResources = routeParamsMock.httpResources; license = routeParamsMock.license; config = routeParamsMock.config; @@ -34,10 +42,10 @@ describe('Login view routes', () => { }); describe('View route', () => { - let routeHandler: RequestHandler; + let routeHandler: HttpResourcesRequestHandler; let routeConfig: RouteConfig; beforeEach(() => { - const [loginRouteConfig, loginRouteHandler] = router.get.mock.calls.find( + const [loginRouteConfig, loginRouteHandler] = httpResources.register.mock.calls.find( ([{ path }]) => path === '/login' )!; @@ -96,9 +104,11 @@ describe('Login view routes', () => { 'https://kibana.co' ); license.getFeatures.mockReturnValue({ showLogin: true } as any); - await expect(routeHandler({} as any, request, kibanaResponseFactory)).resolves.toEqual({ - options: { headers: { location: `${expectedLocation}` } }, - status: 302, + const responseFactory = httpResourcesMock.createResponseFactory(); + + await routeHandler({} as any, request, responseFactory); + expect(responseFactory.redirected).toHaveBeenCalledWith({ + headers: { location: `${expectedLocation}` }, }); // Redirect if `showLogin` is `false` even if user is not authenticated. @@ -108,9 +118,12 @@ describe('Login view routes', () => { 'https://kibana.co' ); license.getFeatures.mockReturnValue({ showLogin: false } as any); - await expect(routeHandler({} as any, request, kibanaResponseFactory)).resolves.toEqual({ - options: { headers: { location: `${expectedLocation}` } }, - status: 302, + responseFactory.redirected.mockClear(); + + await routeHandler({} as any, request, responseFactory); + + expect(responseFactory.redirected).toHaveBeenCalledWith({ + headers: { location: `${expectedLocation}` }, }); } }); @@ -121,19 +134,9 @@ describe('Login view routes', () => { const request = httpServerMock.createKibanaRequest({ auth: { isAuthenticated: false } }); const contextMock = coreMock.createRequestHandlerContext(); - await expect( - routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) - ).resolves.toEqual({ - options: { - headers: { - 'content-security-policy': - "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - }, - }, - status: 200, - }); - - expect(contextMock.rendering.render).toHaveBeenCalledWith({ includeUserSettings: false }); + const responseFactory = httpResourcesMock.createResponseFactory(); + await routeHandler({ core: contextMock } as any, request, responseFactory); + expect(responseFactory.renderAnonymousCoreApp).toHaveBeenCalledWith(); }); }); diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index 4cabd4337971c..4d6747de713f7 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -16,11 +16,11 @@ export function defineLoginRoutes({ config, router, logger, - csp, + httpResources, basePath, license, }: RouteDefinitionParams) { - router.get( + httpResources.register( { path: '/login', validate: { @@ -45,10 +45,7 @@ export function defineLoginRoutes({ }); } - return response.ok({ - body: await context.core.rendering.render({ includeUserSettings: false }), - headers: { 'content-security-policy': csp.header }, - }); + return response.renderAnonymousCoreApp(); } ); diff --git a/x-pack/plugins/security/server/routes/views/logout.ts b/x-pack/plugins/security/server/routes/views/logout.ts index 8fa8e689a1c38..370cb069096a3 100644 --- a/x-pack/plugins/security/server/routes/views/logout.ts +++ b/x-pack/plugins/security/server/routes/views/logout.ts @@ -9,18 +9,9 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for the Logout out view. */ -export function defineLogoutRoutes({ router, csp }: RouteDefinitionParams) { - router.get( - { - path: '/logout', - validate: false, - options: { authRequired: false }, - }, - async (context, request, response) => { - return response.ok({ - body: await context.core.rendering.render({ includeUserSettings: false }), - headers: { 'content-security-policy': csp.header }, - }); - } +export function defineLogoutRoutes({ httpResources }: RouteDefinitionParams) { + httpResources.register( + { path: '/logout', validate: false, options: { authRequired: false } }, + (context, request, response) => response.renderAnonymousCoreApp() ); } diff --git a/x-pack/plugins/security/server/routes/views/overwritten_session.ts b/x-pack/plugins/security/server/routes/views/overwritten_session.ts index c21ab1c207362..ee4988cb122cc 100644 --- a/x-pack/plugins/security/server/routes/views/overwritten_session.ts +++ b/x-pack/plugins/security/server/routes/views/overwritten_session.ts @@ -9,14 +9,9 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for the Overwritten Session view. */ -export function defineOverwrittenSessionRoutes({ router, csp }: RouteDefinitionParams) { - router.get( +export function defineOverwrittenSessionRoutes({ httpResources }: RouteDefinitionParams) { + httpResources.register( { path: '/security/overwritten_session', validate: false }, - async (context, request, response) => { - return response.ok({ - body: await context.core.rendering.render({ includeUserSettings: true }), - headers: { 'content-security-policy': csp.header }, - }); - } + (context, req, res) => res.renderCoreApp() ); } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 72faab0d2c892..d842f07cdb205 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -57,26 +57,26 @@ describe('copySavedObjectsToSpaces', () => { typeRegistry.getAllTypes.mockReturnValue([ { name: 'dashboard', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'visualization', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'globaltype', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: false, mappings: { properties: {} }, }, ]); typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic) + typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceType === 'agnostic') ); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index aa1d5e9a47832..0654712ecff0e 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -57,26 +57,26 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { typeRegistry.getAllTypes.mockReturnValue([ { name: 'dashboard', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'visualization', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'globaltype', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: false, mappings: { properties: {} }, }, ]); typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic) + typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceType === 'agnostic') ); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index 0b9905d5e9c95..75ddee772b168 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -81,7 +81,7 @@ describe('Spaces Plugin', () => { expect(core.savedObjects.registerType).toHaveBeenCalledWith({ name: 'space', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: true, mappings: expect.any(Object), migrations: expect.any(Object), diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index a24d626c2a85d..09b38adb70682 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -115,10 +115,8 @@ export class Plugin { const savedObjectsService = new SpacesSavedObjectsService(); savedObjectsService.setup({ core, spacesService }); - const viewRouter = core.http.createRouter(); initSpacesViewsRoutes({ - viewRouter, - cspHeader: core.http.csp.header, + httpResources: core.http.resources, }); const externalRouter = core.http.createRouter(); diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts index d8c318369834e..cbe832e485d66 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_mock_so_service.ts @@ -46,37 +46,37 @@ export const createMockSavedObjectsService = (spaces: any[] = []) => { typeRegistry.getAllTypes.mockReturnValue([ { name: 'visualization', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'dashboard', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'index-pattern', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }, { name: 'globalType', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: false, mappings: { properties: {} }, }, { name: 'space', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: true, mappings: { properties: {} }, }, ]); typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) => - typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic) + typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceType === 'agnostic') ); savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); diff --git a/x-pack/plugins/spaces/server/routes/views/index.ts b/x-pack/plugins/spaces/server/routes/views/index.ts index 2a346c7e5241a..57ad8872ce558 100644 --- a/x-pack/plugins/spaces/server/routes/views/index.ts +++ b/x-pack/plugins/spaces/server/routes/views/index.ts @@ -4,26 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from 'src/core/server'; +import { HttpResources } from 'src/core/server'; export interface ViewRouteDeps { - viewRouter: IRouter; - cspHeader: string; + httpResources: HttpResources; } export function initSpacesViewsRoutes(deps: ViewRouteDeps) { - deps.viewRouter.get( - { - path: '/spaces/space_selector', - validate: false, - }, - async (context, request, response) => { - return response.ok({ - headers: { - 'Content-Security-Policy': deps.cspHeader, - }, - body: await context.core.rendering.render({ includeUserSettings: true }), - }); - } + deps.httpResources.register( + { path: '/spaces/space_selector', validate: false }, + (context, request, response) => response.renderCoreApp() ); } diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts index 4a9756d9e03f8..31f2c98d74c96 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.test.ts @@ -58,7 +58,7 @@ describe('SpacesSavedObjectsService', () => { "6.6.0": [Function], }, "name": "space", - "namespaceAgnostic": true, + "namespaceType": "agnostic", }, ] `); diff --git a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts index 40ea49573e3c1..58aa1fe08558a 100644 --- a/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts +++ b/x-pack/plugins/spaces/server/saved_objects/saved_objects_service.ts @@ -20,7 +20,7 @@ export class SpacesSavedObjectsService { core.savedObjects.registerType({ name: 'space', hidden: true, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: SpacesSavedObjectMappings, migrations: { '6.6.0': migrateToKibana660, diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index f9961329c088b..569219a0b8990 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -13,21 +13,21 @@ import { SavedObjectTypeRegistry } from 'src/core/server'; const typeRegistry = new SavedObjectTypeRegistry(); typeRegistry.registerType({ name: 'foo', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }); typeRegistry.registerType({ name: 'bar', - namespaceAgnostic: false, + namespaceType: 'single', hidden: false, mappings: { properties: {} }, }); typeRegistry.registerType({ name: 'space', - namespaceAgnostic: true, + namespaceType: 'agnostic', hidden: true, mappings: { properties: {} }, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0d852318d2c1c..72b55121bbec0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2004,7 +2004,6 @@ "kbn.context.reloadPageDescription.selectValidAnchorDocumentTextMessage": "にアクセスして有効な別のドキュメントを選択してください。", "kbn.context.unableToLoadAnchorDocumentDescription": "別のドキュメントが読み込めません", "kbn.context.unableToLoadDocumentDescription": "ドキュメントが読み込めません", - "kbn.dashboard.listing.table.descriptionColumnName": "説明", "kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}", "kbn.dashboardTitle": "ダッシュボード", @@ -2421,45 +2420,6 @@ "savedObjectsManagement.field.offLabel": "オフ", "savedObjectsManagement.field.onLabel": "オン", "kbn.managementTitle": "管理", - "kbn.topNavMenu.openInspectorButtonLabel": "検査", - "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", - "kbn.topNavMenu.shareVisualizationButtonLabel": "共有", - "kbn.visualize.badge.readOnly.text": "読み込み専用", - "kbn.visualize.badge.readOnly.tooltip": "ビジュアライゼーションを保存できません", - "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPattern または savedSearchId が必要です", - "kbn.visualize.editor.createBreadcrumb": "作成", - "kbn.visualize.experimentalVisInfoText": "このビジュアライゼーションは実験的なものです。", - "kbn.visualize.helpMenu.appName": "可視化", - "kbn.visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", - "kbn.visualize.listing.betaTitle": "ベータ", - "kbn.visualize.listing.betaTooltip": "このビジュアライゼーションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません", - "kbn.visualize.listing.breadcrumb": "可視化", - "kbn.visualize.listing.createNew.createButtonLabel": "新規ビジュアライゼーションを追加", - "kbn.visualize.listing.createNew.description": "データに基づき異なるビジュアライゼーションを作成できます。", - "kbn.visualize.listing.createNew.title": "最初のビジュアライゼーションの作成", - "kbn.visualize.listing.experimentalTitle": "実験的", - "kbn.visualize.listing.experimentalTooltip": "このビジュアライゼーションは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", - "kbn.visualize.listing.noItemsMessage": "ビジュアライゼーションがないようです。", - "kbn.visualize.listing.table.entityName": "ビジュアライゼーション", - "kbn.visualize.listing.table.entityNamePlural": "ビジュアライゼーション", - "kbn.visualize.listing.table.listTitle": "ビジュアライゼーション", - "kbn.visualize.listing.table.titleColumnName": "タイトル", - "kbn.visualize.listing.table.typeColumnName": "タイプ", - "kbn.visualize.pageHeading": "{chartName} {chartType} ビジュアライゼーション", - "kbn.visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存してダッシュボードに追加", - "kbn.visualize.topNavMenu.openInspectorButtonAriaLabel": "ビジュアライゼーションのインスペクターを開く", - "kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip": "このビジュアライゼーションはインスペクターをサポートしていません。", - "kbn.visualize.topNavMenu.saveVisualization.failureNotificationText": "「{visTitle}」の保存中にエラーが発生しました", - "kbn.visualize.topNavMenu.saveVisualization.successNotificationText": "「{visTitle}」が保存されました", - "kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel": "ビジュアライゼーションを保存", - "kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "保存する前に変更を適用または破棄", - "kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel": "ビジュアライゼーションを共有", - "kbn.visualize.visualizationTypeInvalidNotificationMessage": "無効なビジュアライゼーションタイプ", - "kbn.visualize.visualizeDescription": "ビジュアライゼーションを作成して Elasticsearch インデックスに保存されたデータを集約します。", - "kbn.visualize.visualizeListingBreadcrumbsTitle": "可視化", - "kbn.visualize.visualizeListingDeleteErrorTitle": "ビジュアライゼーションの削除中にエラーが発生", - "kbn.visualize.wizard.step1Breadcrumb": "作成", - "kbn.visualize.wizard.step2Breadcrumb": "作成", "kbn.visualizeTitle": "可視化", "kibana_legacy.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", @@ -4021,6 +3981,46 @@ "visualizations.newVisWizard.visTypeAliasDescription": "Visualize 外で Kibana アプリケーションを開きます。", "visualizations.newVisWizard.visTypeAliasTitle": "Kibana アプリケーション", "visualizations.savedObjectName": "ビジュアライゼーション", + "visualize.listing.table.descriptionColumnName": "説明", + "visualize.topNavMenu.openInspectorButtonLabel": "検査", + "visualize.topNavMenu.saveVisualizationButtonLabel": "保存", + "visualize.topNavMenu.shareVisualizationButtonLabel": "共有", + "visualize.badge.readOnly.text": "読み込み専用", + "visualize.badge.readOnly.tooltip": "ビジュアライゼーションを保存できません", + "visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPattern または savedSearchId が必要です", + "visualize.editor.createBreadcrumb": "作成", + "visualize.experimentalVisInfoText": "このビジュアライゼーションは実験的なものです。", + "visualize.helpMenu.appName": "可視化", + "visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", + "visualize.listing.betaTitle": "ベータ", + "visualize.listing.betaTooltip": "このビジュアライゼーションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません", + "visualize.listing.breadcrumb": "可視化", + "visualize.listing.createNew.createButtonLabel": "新規ビジュアライゼーションを追加", + "visualize.listing.createNew.description": "データに基づき異なるビジュアライゼーションを作成できます。", + "visualize.listing.createNew.title": "最初のビジュアライゼーションの作成", + "visualize.listing.experimentalTitle": "実験的", + "visualize.listing.experimentalTooltip": "このビジュアライゼーションは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。", + "visualize.listing.noItemsMessage": "ビジュアライゼーションがないようです。", + "visualize.listing.table.entityName": "ビジュアライゼーション", + "visualize.listing.table.entityNamePlural": "ビジュアライゼーション", + "visualize.listing.table.listTitle": "ビジュアライゼーション", + "visualize.listing.table.titleColumnName": "タイトル", + "visualize.listing.table.typeColumnName": "タイプ", + "visualize.pageHeading": "{chartName} {chartType} ビジュアライゼーション", + "visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存してダッシュボードに追加", + "visualize.topNavMenu.openInspectorButtonAriaLabel": "ビジュアライゼーションのインスペクターを開く", + "visualize.topNavMenu.openInspectorDisabledButtonTooltip": "このビジュアライゼーションはインスペクターをサポートしていません。", + "visualize.topNavMenu.saveVisualization.failureNotificationText": "「{visTitle}」の保存中にエラーが発生しました", + "visualize.topNavMenu.saveVisualization.successNotificationText": "「{visTitle}」が保存されました", + "visualize.topNavMenu.saveVisualizationButtonAriaLabel": "ビジュアライゼーションを保存", + "visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "保存する前に変更を適用または破棄", + "visualize.topNavMenu.shareVisualizationButtonAriaLabel": "ビジュアライゼーションを共有", + "visualize.visualizationTypeInvalidNotificationMessage": "無効なビジュアライゼーションタイプ", + "visualize.visualizeDescription": "ビジュアライゼーションを作成して Elasticsearch インデックスに保存されたデータを集約します。", + "visualize.visualizeListingBreadcrumbsTitle": "可視化", + "visualize.visualizeListingDeleteErrorTitle": "ビジュアライゼーションの削除中にエラーが発生", + "visualize.wizard.step1Breadcrumb": "作成", + "visualize.wizard.step2Breadcrumb": "作成", "xpack.actions.actionTypeRegistry.get.missingActionTypeErrorMessage": "アクションタイプ \"{id}\" は登録されていません。", "xpack.actions.actionTypeRegistry.register.duplicateActionTypeErrorMessage": "アクションタイプ \"{id}\" は既に登録されています。", "xpack.actions.appName": "アクション", @@ -9575,8 +9575,6 @@ "xpack.ml.dataframe.analytics.regressionExploration.generalError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー", - "xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent": "配列", - "xpack.ml.dataframe.analytics.regressionExploration.indexArrayToolTipContent": "この配列ベースの列の完全なコンテンツは表示できません。", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel": "テスト", "xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel": "トレーニング", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a19cbe4382816..c1b4663da91e7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2005,7 +2005,6 @@ "kbn.context.reloadPageDescription.selectValidAnchorDocumentTextMessage": "以选择有效地定位点文档。", "kbn.context.unableToLoadAnchorDocumentDescription": "无法加载该定位点文档", "kbn.context.unableToLoadDocumentDescription": "无法加载文档", - "kbn.dashboard.listing.table.descriptionColumnName": "描述", "kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", "kbn.dashboardTitle": "仪表板", @@ -2422,45 +2421,6 @@ "savedObjectsManagement.breadcrumb.edit": "编辑 {savedObjectType}", "savedObjectsManagement.breadcrumb.index": "已保存对象", "kbn.managementTitle": "管理", - "kbn.topNavMenu.openInspectorButtonLabel": "检查", - "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", - "kbn.topNavMenu.shareVisualizationButtonLabel": "共享", - "kbn.visualize.badge.readOnly.text": "只读", - "kbn.visualize.badge.readOnly.tooltip": "无法保存可视化", - "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", - "kbn.visualize.editor.createBreadcrumb": "创建", - "kbn.visualize.experimentalVisInfoText": "此可视化标记为“实验”。", - "kbn.visualize.helpMenu.appName": "Visualize", - "kbn.visualize.linkedToSearch.unlinkSuccessNotificationText": "取消与已保存搜索 “{searchTitle}” 的链接", - "kbn.visualize.listing.betaTitle": "公测版", - "kbn.visualize.listing.betaTooltip": "此可视化为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束", - "kbn.visualize.listing.breadcrumb": "可视化", - "kbn.visualize.listing.createNew.createButtonLabel": "新建可视化", - "kbn.visualize.listing.createNew.description": "可以根据您的数据创建不同的可视化。", - "kbn.visualize.listing.createNew.title": "创建首个可视化", - "kbn.visualize.listing.experimentalTitle": "实验性", - "kbn.visualize.listing.experimentalTooltip": "未来版本可能会更改或删除此可视化,其不受支持 SLA 的约束。", - "kbn.visualize.listing.noItemsMessage": "看起来您还没有任何可视化。", - "kbn.visualize.listing.table.entityName": "可视化", - "kbn.visualize.listing.table.entityNamePlural": "可视化", - "kbn.visualize.listing.table.listTitle": "可视化", - "kbn.visualize.listing.table.titleColumnName": "标题", - "kbn.visualize.listing.table.typeColumnName": "类型", - "kbn.visualize.pageHeading": "{chartName} {chartType}可视化", - "kbn.visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存并添加到仪表板", - "kbn.visualize.topNavMenu.openInspectorButtonAriaLabel": "打开检查器查看可视化", - "kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip": "此可视化不支持任何检查器。", - "kbn.visualize.topNavMenu.saveVisualization.failureNotificationText": "保存 “{visTitle}” 时出错", - "kbn.visualize.topNavMenu.saveVisualization.successNotificationText": "已保存“{visTitle}”", - "kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel": "保存可视化", - "kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "应用或放弃所做更改,然后保存", - "kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel": "共享可视化", - "kbn.visualize.visualizationTypeInvalidNotificationMessage": "无效的可视化类型", - "kbn.visualize.visualizeDescription": "创建可视化并聚合存储在 Elasticsearch 索引中的数据。", - "kbn.visualize.visualizeListingBreadcrumbsTitle": "可视化", - "kbn.visualize.visualizeListingDeleteErrorTitle": "删除可视化时出错", - "kbn.visualize.wizard.step1Breadcrumb": "创建", - "kbn.visualize.wizard.step2Breadcrumb": "创建", "kbn.visualizeTitle": "可视化", "kibana_legacy.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", @@ -4022,6 +3982,46 @@ "visualizations.newVisWizard.visTypeAliasDescription": "打开 Visualize 外部的 Kibana 应用程序。", "visualizations.newVisWizard.visTypeAliasTitle": "Kibana 应用程序", "visualizations.savedObjectName": "可视化", + "visualize.listing.table.descriptionColumnName": "描述", + "visualize.topNavMenu.openInspectorButtonLabel": "检查", + "visualize.topNavMenu.saveVisualizationButtonLabel": "保存", + "visualize.topNavMenu.shareVisualizationButtonLabel": "共享", + "visualize.badge.readOnly.text": "只读", + "visualize.badge.readOnly.tooltip": "无法保存可视化", + "visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", + "visualize.editor.createBreadcrumb": "创建", + "visualize.experimentalVisInfoText": "此可视化标记为“实验”。", + "visualize.helpMenu.appName": "Visualize", + "visualize.linkedToSearch.unlinkSuccessNotificationText": "取消与已保存搜索 “{searchTitle}” 的链接", + "visualize.listing.betaTitle": "公测版", + "visualize.listing.betaTooltip": "此可视化为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束", + "visualize.listing.breadcrumb": "可视化", + "visualize.listing.createNew.createButtonLabel": "新建可视化", + "visualize.listing.createNew.description": "可以根据您的数据创建不同的可视化。", + "visualize.listing.createNew.title": "创建首个可视化", + "visualize.listing.experimentalTitle": "实验性", + "visualize.listing.experimentalTooltip": "未来版本可能会更改或删除此可视化,其不受支持 SLA 的约束。", + "visualize.listing.noItemsMessage": "看起来您还没有任何可视化。", + "visualize.listing.table.entityName": "可视化", + "visualize.listing.table.entityNamePlural": "可视化", + "visualize.listing.table.listTitle": "可视化", + "visualize.listing.table.titleColumnName": "标题", + "visualize.listing.table.typeColumnName": "类型", + "visualize.pageHeading": "{chartName} {chartType}可视化", + "visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存并添加到仪表板", + "visualize.topNavMenu.openInspectorButtonAriaLabel": "打开检查器查看可视化", + "visualize.topNavMenu.openInspectorDisabledButtonTooltip": "此可视化不支持任何检查器。", + "visualize.topNavMenu.saveVisualization.failureNotificationText": "保存 “{visTitle}” 时出错", + "visualize.topNavMenu.saveVisualization.successNotificationText": "已保存“{visTitle}”", + "visualize.topNavMenu.saveVisualizationButtonAriaLabel": "保存可视化", + "visualize.topNavMenu.saveVisualizationDisabledButtonTooltip": "应用或放弃所做更改,然后保存", + "visualize.topNavMenu.shareVisualizationButtonAriaLabel": "共享可视化", + "visualize.visualizationTypeInvalidNotificationMessage": "无效的可视化类型", + "visualize.visualizeDescription": "创建可视化并聚合存储在 Elasticsearch 索引中的数据。", + "visualize.visualizeListingBreadcrumbsTitle": "可视化", + "visualize.visualizeListingDeleteErrorTitle": "删除可视化时出错", + "visualize.wizard.step1Breadcrumb": "创建", + "visualize.wizard.step2Breadcrumb": "创建", "xpack.actions.actionTypeRegistry.get.missingActionTypeErrorMessage": "未注册操作类型“{id}”。", "xpack.actions.actionTypeRegistry.register.duplicateActionTypeErrorMessage": "操作类型“{id}”已注册。", "xpack.actions.appName": "操作", @@ -9578,8 +9578,6 @@ "xpack.ml.dataframe.analytics.regressionExploration.generalError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差", - "xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent": "数组", - "xpack.ml.dataframe.analytics.regressionExploration.indexArrayToolTipContent": "此基于数组的列的完整内容无法显示。", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "加载索引数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel": "测试", "xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel": "培训", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 41564146bb84d..488db42f4e843 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -87,6 +87,14 @@ describe('action_form', () => { config: {}, isPreconfigured: false, }, + { + secrets: {}, + id: 'test2', + actionTypeId: actionType.id, + name: 'Test connector 2', + config: {}, + isPreconfigured: true, + }, ]); const mockes = coreMock.createSetup(); deps = { @@ -100,6 +108,7 @@ describe('action_form', () => { disabledByLicenseActionType, ]); actionTypeRegistry.has.mockReturnValue(true); + actionTypeRegistry.get.mockReturnValue(actionType); const initialAlert = ({ name: 'test', @@ -206,6 +215,29 @@ describe('action_form', () => { expect(actionOption.exists()).toBeFalsy(); }); + it(`renders available connectors for the selected action type`, async () => { + await setup(); + const actionOption = wrapper.find( + `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` + ); + actionOption.first().simulate('click'); + const combobox = wrapper.find(`[data-test-subj="selectActionConnector"]`); + expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "id": "test", + "key": "test", + "label": "Test connector ", + }, + Object { + "id": "test2", + "key": "test2", + "label": "Test connector 2 (pre-configured)", + }, + ] + `); + }); + it('renders action types disabled by license', async () => { await setup(); const actionOption = wrapper.find( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index f0c922bb3a1e1..3f964cc167c25 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -164,6 +164,7 @@ export const ActionForm = ({ label: optionTitle, value: optionTitle, id: actionItemId, + 'data-test-subj': 'itemActionConnector', }, ]; }; @@ -177,13 +178,9 @@ export const ActionForm = ({ index: number ) => { const optionsList = connectors - .filter( - connectorItem => - connectorItem.actionTypeId === actionItem.actionTypeId && - connectorItem.id === actionItem.id - ) - .map(({ name, id }) => ({ - label: name, + .filter(connectorItem => connectorItem.actionTypeId === actionItem.actionTypeId) + .map(({ name, id, isPreconfigured }) => ({ + label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`, key: id, id, })); @@ -231,6 +228,8 @@ export const ActionForm = ({ fullWidth singleSelection={{ asPlainText: true }} options={optionsList} + id={`selectActionConnector-${actionItem.id}`} + data-test-subj="selectActionConnector" selectedOptions={getSelectedOptions(actionItem.id)} onChange={selectedOptions => { setActionIdByIndex(selectedOptions[0].id ?? '', index); @@ -448,6 +447,7 @@ export const ActionForm = ({ const actionTypeConnectors = connectors.filter( field => field.actionTypeId === actionTypeModel.id ); + if (actionTypeConnectors.length > 0) { actions.push({ id: '', diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 2eb17d588d297..2cc6f23ebaae5 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -88,6 +88,10 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { + "certificatesThresholds": Object { + "errorState": 7, + "warningState": 30, + }, "heartbeatIndices": "heartbeat-8*", }, "locations": Array [], @@ -131,6 +135,10 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { + "certificatesThresholds": Object { + "errorState": 7, + "warningState": 30, + }, "heartbeatIndices": "heartbeat-8*", }, "locations": Array [], diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts index 175634ef797cc..3ccfd498c44bf 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -7,14 +7,10 @@ import { DynamicSettings, defaultDynamicSettings, -} from '../../../../legacy/plugins/uptime/common/runtime_types/dynamic_settings'; +} from '../../../../legacy/plugins/uptime/common/runtime_types'; import { SavedObjectsType, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { UMSavedObjectsQueryFn } from './adapters'; -export interface UMDynamicSettingsType { - heartbeatIndices: string; -} - export interface UMSavedObjectsAdapter { getUptimeDynamicSettings: UMSavedObjectsQueryFn; setUptimeDynamicSettings: UMSavedObjectsQueryFn; @@ -26,12 +22,22 @@ export const settingsObjectId = 'uptime-dynamic-settings-singleton'; export const umDynamicSettings: SavedObjectsType = { name: settingsObjectType, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: { properties: { heartbeatIndices: { type: 'keyword', }, + certificatesThresholds: { + properties: { + errorState: { + type: 'long', + }, + warningState: { + type: 'long', + }, + }, + }, }, }, }; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 061c9e4a0d921..974f3eb6db60f 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -4,13 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -const alwaysImportedTests = [require.resolve('../test/functional/config.js')]; +const alwaysImportedTests = [ + require.resolve('../test/functional/config.js'), + require.resolve('../test/functional_endpoint_ingest_failure/config.ts'), + require.resolve('../test/functional_endpoint/config.ts'), + require.resolve('../test/functional_with_es_ssl/config.ts'), + require.resolve('../test/functional/config_security_basic.js'), + require.resolve('../test/plugin_functional/config.ts'), +]; const onlyNotInCoverageTests = [ require.resolve('../test/reporting/configs/chromium_api.js'), require.resolve('../test/reporting/configs/chromium_functional.js'), require.resolve('../test/reporting/configs/generate_api.js'), - require.resolve('../test/functional_with_es_ssl/config.ts'), - require.resolve('../test/functional/config_security_basic.js'), require.resolve('../test/api_integration/config_security_basic.js'), require.resolve('../test/api_integration/config.js'), require.resolve('../test/alerting_api_integration/basic/config.ts'), @@ -18,7 +23,6 @@ const onlyNotInCoverageTests = [ require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), require.resolve('../test/plugin_api_integration/config.ts'), - require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/kerberos_api_integration/config.ts'), require.resolve('../test/kerberos_api_integration/anonymous_access.config.ts'), require.resolve('../test/saml_api_integration/config.ts'), @@ -43,8 +47,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/licensing_plugin/config.ts'), require.resolve('../test/licensing_plugin/config.public.ts'), require.resolve('../test/licensing_plugin/config.legacy.ts'), - require.resolve('../test/functional_endpoint_ingest_failure/config.ts'), - require.resolve('../test/functional_endpoint/config.ts'), + require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), ]; require('@kbn/plugin-helpers').babelRegister(); diff --git a/x-pack/test/api_integration/apis/endpoint/index.ts b/x-pack/test/api_integration/apis/endpoint/index.ts index 4ffd0c3b6044b..0a5f9aa595b8a 100644 --- a/x-pack/test/api_integration/apis/endpoint/index.ts +++ b/x-pack/test/api_integration/apis/endpoint/index.ts @@ -6,9 +6,17 @@ import { FtrProviderContext } from '../../ftr_provider_context'; -export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { +export default function endpointAPIIntegrationTests({ + loadTestFile, + getService, +}: FtrProviderContext) { describe('Endpoint plugin', function() { + const ingestManager = getService('ingestManager'); this.tags(['endpoint']); + before(async () => { + await ingestManager.setup(); + }); + loadTestFile(require.resolve('./index_pattern')); loadTestFile(require.resolve('./resolver')); loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./alerts')); diff --git a/x-pack/test/api_integration/apis/endpoint/index_pattern.ts b/x-pack/test/api_integration/apis/endpoint/index_pattern.ts new file mode 100644 index 0000000000000..d3ffd67defef1 --- /dev/null +++ b/x-pack/test/api_integration/apis/endpoint/index_pattern.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 expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Endpoint index pattern API', () => { + it('should retrieve the index pattern for events', async () => { + const { body } = await supertest.get('/api/endpoint/index_pattern/events').expect(200); + expect(body.indexPattern).to.eql('events-endpoint-*'); + }); + + it('should retrieve the index pattern for metadata', async () => { + const { body } = await supertest.get('/api/endpoint/index_pattern/metadata').expect(200); + expect(body.indexPattern).to.eql('metrics-endpoint-*'); + }); + + it('should not retrieve the index pattern for an invalid key', async () => { + await supertest.get('/api/endpoint/index_pattern/blah').expect(404); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 887be6b85b100..943782c79ada7 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -159,7 +159,7 @@ export default function({ getService }: FtrProviderContext) { .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') .send({ - filter: `host.os.variant.keyword:${variantValue}`, + filter: `host.os.variant:${variantValue}`, }) .expect(200); expect(body.total).to.eql(2); diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index c1e9240c09951..e8d336e875b99 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -17,8 +17,8 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC const esArchiver = getService('esArchiver'); describe('Resolver', () => { - before(() => esArchiver.load('endpoint/resolver/api_feature')); - after(() => esArchiver.unload('endpoint/resolver/api_feature')); + before(async () => await esArchiver.load('endpoint/resolver/api_feature')); + after(async () => await esArchiver.unload('endpoint/resolver/api_feature')); describe('related events endpoint', () => { const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.helpers.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.helpers.js index dbcb6bf819749..294db29f6dce4 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.helpers.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.helpers.js @@ -13,7 +13,45 @@ export const registerHelpers = supertest => { const loadFollowerIndices = () => supertest.get(`${API_BASE_PATH}/follower_indices`); - const getFollowerIndex = name => supertest.get(`${API_BASE_PATH}/follower_indices/${name}`); + const getFollowerIndex = (name, waitUntilIsActive = false) => { + const maxRetries = 10; + const delayBetweenRetries = 500; + let retryCount = 0; + + const proceed = async () => { + const response = await supertest.get(`${API_BASE_PATH}/follower_indices/${name}`); + + if (waitUntilIsActive && response.body.status !== 'active') { + retryCount += 1; + + if (retryCount > maxRetries) { + throw new Error('Error waiting for follower index to be active.'); + } + + return new Promise(resolve => setTimeout(resolve, delayBetweenRetries)).then(proceed); + } + + return response; + }; + + return { + expect: status => + new Promise((resolve, reject) => + proceed() + .then(response => { + if (status !== response.status) { + reject(new Error(`Expected status ${status} but got ${response.status}`)); + } + return resolve(response); + }) + .catch(reject) + ), + then: (resolve, reject) => + proceed() + .then(resolve) + .catch(reject), + }; + }; const createFollowerIndex = (name = getRandomString(), payload = getFollowerIndexPayload()) => { followerIndicesCreated.push(name); @@ -24,6 +62,13 @@ export const registerHelpers = supertest => { .send({ ...payload, name }); }; + const updateFollowerIndex = (name, payload) => { + return supertest + .put(`${API_BASE_PATH}/follower_indices/${name}`) + .set('kbn-xsrf', 'xxx') + .send(payload); + }; + const unfollowLeaderIndex = followerIndex => { const followerIndices = Array.isArray(followerIndex) ? followerIndex : [followerIndex]; const followerIndicesToEncodedString = followerIndices @@ -51,6 +96,7 @@ export const registerHelpers = supertest => { loadFollowerIndices, getFollowerIndex, createFollowerIndex, + updateFollowerIndex, unfollowAll, }; }; diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js index 5f9ebbd2a0a3f..eabf474120f2b 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js @@ -21,6 +21,7 @@ export default function({ getService }) { loadFollowerIndices, getFollowerIndex, createFollowerIndex, + updateFollowerIndex, unfollowAll, } = registerFollowerIndicesnHelpers(supertest); @@ -92,6 +93,31 @@ export default function({ getService }) { }); }); + describe('update()', () => { + it('should update a follower index advanced settings', async () => { + // Create a follower index + const leaderIndex = await createIndex(); + const followerIndex = getRandomString(); + const initialValue = 1234; + const payload = getFollowerIndexPayload(leaderIndex, undefined, { + maxReadRequestOperationCount: initialValue, + }); + await createFollowerIndex(followerIndex, payload); + + // Verify that its advanced settings are correctly set + const { body } = await getFollowerIndex(followerIndex, true); + expect(body.maxReadRequestOperationCount).to.be(initialValue); + + // Update the follower index + const updatedValue = 7777; + await updateFollowerIndex(followerIndex, { maxReadRequestOperationCount: updatedValue }); + + // Verify that the advanced settings are updated + const { body: updatedBody } = await getFollowerIndex(followerIndex, true); + expect(updatedBody.maxReadRequestOperationCount).to.be(updatedValue); + }); + }); + describe('Advanced settings', () => { it('hard-coded values should match Elasticsearch default values', async () => { /** diff --git a/x-pack/test/api_integration/apis/maps/migrations.js b/x-pack/test/api_integration/apis/maps/migrations.js index c4587530e160b..cd575899118a3 100644 --- a/x-pack/test/api_integration/apis/maps/migrations.js +++ b/x-pack/test/api_integration/apis/maps/migrations.js @@ -41,7 +41,7 @@ export default function({ getService }) { type: 'index-pattern', }, ]); - expect(resp.body.migrationVersion).to.eql({ map: '7.7.0' }); + expect(resp.body.migrationVersion).to.eql({ map: '7.8.0' }); expect(resp.body.attributes.layerListJSON.includes('indexPatternRefName')).to.be(true); }); }); diff --git a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts index f4dd7c244f8b5..a1b731169f0a0 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts @@ -18,7 +18,13 @@ export default function({ getService }: FtrProviderContext) { }); it('can change the settings', async () => { - const newSettings = { heartbeatIndices: 'myIndex1*' }; + const newSettings = { + heartbeatIndices: 'myIndex1*', + certificatesThresholds: { + errorState: 5, + warningState: 15, + }, + }; const postResponse = await supertest .post(`/api/uptime/dynamic_settings`) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index 9c945f557a2d8..84b8476bd1dd1 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -22,6 +22,7 @@ import { import { SiemGraphQLClientProvider, SiemGraphQLClientFactoryProvider } from './siem_graphql_client'; import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration'; import { MachineLearningProvider } from './ml'; +import { IngestManagerProvider } from './ingest_manager'; export const services = { ...commonServices, @@ -39,4 +40,5 @@ export const services = { supertestWithoutAuth: SupertestWithoutAuthProvider, usageAPI: UsageAPIProvider, ml: MachineLearningProvider, + ingestManager: IngestManagerProvider, }; diff --git a/x-pack/test/api_integration/services/ingest_manager.ts b/x-pack/test/api_integration/services/ingest_manager.ts new file mode 100644 index 0000000000000..2b70a20ca0362 --- /dev/null +++ b/x-pack/test/api_integration/services/ingest_manager.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../ftr_provider_context'; +import { setupRouteService, fleetSetupRouteService } from '../../../plugins/ingest_manager/common'; + +export function IngestManagerProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + return { + async setup() { + const headers = { accept: 'application/json', 'kbn-xsrf': 'some-xsrf-token' }; + + const { body } = await supertest + .get(fleetSetupRouteService.getFleetSetupPath()) + .set(headers) + .expect(200); + + if (!body.isInitialized) { + await supertest + .post(setupRouteService.getSetupPath()) + .set(headers) + .expect(200); + } + }, + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts new file mode 100644 index 0000000000000..45805f03f8c0c --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + deleteAllRulesStatuses, + getSimpleRule, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('find_statuses', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllRulesStatuses(es); + }); + + it('should return an empty find statuses body correctly if no statuses are loaded', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [] }) + .expect(200); + + expect(body).to.eql({}); + }); + + it('should return a single rule status when a single rule is loaded from a find status with defaults added', async () => { + // add a single rule + const { body: resBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + await new Promise(resolve => setTimeout(resolve, 1000)).then(async () => { + // query the single rule from _find + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + + // expected result for status should be 'going to run' or 'succeeded + expect(['succeeded', 'going to run']).to.contain(body[resBody.id].current_status.status); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index b8034fd92e988..917654e50cb99 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -18,6 +18,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); loadTestFile(require.resolve('./find_rules')); + loadTestFile(require.resolve('./find_statuses')); loadTestFile(require.resolve('./get_prepackaged_rules_status')); loadTestFile(require.resolve('./import_rules')); loadTestFile(require.resolve('./read_rules')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts index 7b725a7830c56..0a5b2def3eb18 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts @@ -196,6 +196,19 @@ export const deleteAllAlerts = async (es: any): Promise => { }); }; +/** + * Remove all rules statuses from the .kibana index + * @param es The ElasticSearch handle + */ +export const deleteAllRulesStatuses = async (es: any): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:siem-detection-engine-rule-status', + waitForCompletion: true, + refresh: 'wait_for', + }); +}; + /** * Creates the signals index for use inside of beforeEach blocks of tests * @param supertest The supertest client library diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts index 170b7e0c6d09d..1a01cf8929c05 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts @@ -29,7 +29,7 @@ export const plugin: PluginInitializer = core.savedObjects.registerType({ name: SAVED_OBJECT_WITH_SECRET_TYPE, hidden: false, - namespaceAgnostic: false, + namespaceType: 'single', mappings: deepFreeze({ properties: { publicProperty: { type: 'keyword' }, diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/alerts.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/alerts.ts new file mode 100644 index 0000000000000..b75d69238d653 --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/alerts.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({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('Endpoint alert API without ingest manager initialized', () => { + before(async () => { + await esArchiver.load('endpoint/alerts/api_feature'); + await esArchiver.load('endpoint/alerts/host_api_feature'); + }); + + after(async () => { + await esArchiver.unload('endpoint/alerts/api_feature'); + await esArchiver.unload('endpoint/alerts/host_api_feature'); + }); + + it('should return a 500', async () => { + await supertest.get('/api/endpoint/alerts').expect(500); + }); + }); +} diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts new file mode 100644 index 0000000000000..6110f398df5a0 --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('Endpoint plugin', function() { + this.tags('ciGroup7'); + loadTestFile(require.resolve('./index_pattern')); + loadTestFile(require.resolve('./metadata')); + loadTestFile(require.resolve('./alerts')); + }); +} diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/index_pattern.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/index_pattern.ts new file mode 100644 index 0000000000000..664ef7d96847c --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/index_pattern.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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Endpoint index pattern API without ingest manager initialized', () => { + it('should not retrieve the index pattern for events', async () => { + await supertest.get('/api/endpoint/index_pattern/events').expect(404); + }); + }); +} diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/metadata.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/metadata.ts new file mode 100644 index 0000000000000..886d3cf3d9516 --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/metadata.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + describe('test metadata api when ingest manager is not initialized', () => { + before(async () => await esArchiver.load('endpoint/metadata/api_feature')); + after(async () => await esArchiver.unload('endpoint/metadata/api_feature')); + it('metadata api should return a 500', async () => { + await supertest + .post('/api/endpoint/metadata') + .set('kbn-xsrf', 'xxx') + .send() + .expect(500); + }); + }); +} diff --git a/x-pack/test/endpoint_api_integration_no_ingest/config.ts b/x-pack/test/endpoint_api_integration_no_ingest/config.ts new file mode 100644 index 0000000000000..bf8b68a7e991c --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/config.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 { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + + return { + ...xPackAPITestsConfig.getAll(), + testFiles: [require.resolve('./apis')], + junit: { + reportName: 'X-Pack Endpoint API Integration Without Ingest Tests', + }, + }; +} diff --git a/x-pack/test/endpoint_api_integration_no_ingest/ftr_provider_context.d.ts b/x-pack/test/endpoint_api_integration_no_ingest/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..2751dbcdc6539 --- /dev/null +++ b/x-pack/test/endpoint_api_integration_no_ingest/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 '../api_integration/services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 89a6c6ea82e53..4b36109a4de9c 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -9,7 +9,7 @@ import _ from 'lodash'; import { MAPBOX_STYLES } from './mapbox_styles'; -const JOIN_PROPERTY_NAME = '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name'; +const JOIN_PROPERTY_NAME = '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'; const EXPECTED_JOIN_VALUES = { alpha: 10, bravo: 3, diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index 508a019db1764..63bfc331d8886 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -27,7 +27,7 @@ export const MAPBOX_STYLES = { 'case', [ '==', - ['feature-state', '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name'], + ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], null, ], 2, @@ -39,7 +39,7 @@ export const MAPBOX_STYLES = { 'to-number', [ 'feature-state', - '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], ], 12, @@ -97,7 +97,7 @@ export const MAPBOX_STYLES = { 'case', [ '==', - ['feature-state', '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name'], + ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], null, ], 2, @@ -109,7 +109,7 @@ export const MAPBOX_STYLES = { 'to-number', [ 'feature-state', - '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], ], 12, diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index 3294d928b61b3..64cfee50ac982 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -74,7 +74,41 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify that the settings page shows the value we previously saved await settings.go(); const fields = await settings.loadFields(); - expect(fields).to.eql(newFieldValues); + expect(fields.heartbeatIndices).to.eql(newFieldValues.heartbeatIndices); + }); + + it('changing certificate expiration error threshold is reflected in settings page', async () => { + const settings = uptimeService.settings; + + await settings.go(); + + const newErrorThreshold = '5'; + await settings.changeErrorThresholdInput(newErrorThreshold); + await settings.apply(); + + await uptimePage.goToRoot(); + + // Verify that the settings page shows the value we previously saved + await settings.go(); + const fields = await settings.loadFields(); + expect(fields.certificatesThresholds.errorState).to.eql(newErrorThreshold); + }); + + it('changing certificate expiration warning threshold is reflected in settings page', async () => { + const settings = uptimeService.settings; + + await settings.go(); + + const newWarningThreshold = '15'; + await settings.changeWarningThresholdInput(newWarningThreshold); + await settings.apply(); + + await uptimePage.goToRoot(); + + // Verify that the settings page shows the value we previously saved + await settings.go(); + const fields = await settings.loadFields(); + expect(fields.certificatesThresholds.warningState).to.eql(newWarningThreshold); }); }); }; diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts index 4f12dd16247f6..cbd03110b0f14 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { VisualizeConstants } from '../../../../../../src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_constants'; +import { VisualizeConstants } from '../../../../../../src/plugins/visualize/public/application/visualize_constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/config.edge.js b/x-pack/test/functional/config.edge.js new file mode 100644 index 0000000000000..882fb6fea3686 --- /dev/null +++ b/x-pack/test/functional/config.edge.js @@ -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. + */ + +export default async function({ readConfigFile }) { + const chromeConfig = await readConfigFile(require.resolve('./config')); + + return { + ...chromeConfig.getAll(), + + browser: { + type: 'msedge', + }, + + junit: { + reportName: 'MS Chromium Edge XPack UI Functional Tests', + }, + }; +} diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz index 94a96c54ee9cb..a71281c0ecfec 100644 Binary files a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz and b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json index 61ddf3c4e65db..f9d5de0d0a94c 100644 --- a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/mappings.json @@ -1,33 +1,62 @@ { "type": "index", "value": { - "aliases": { - }, - "index": "endpoint-agent-1", + "aliases": {}, + "index": "metrics-endpoint-default-1", "mappings": { + "_meta": { + "version": "1.5.0-dev" + }, + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], "properties": { "@timestamp": { - "type": "long" + "type": "date" }, - "agent": { + "elastic": { "properties": { - "id": { - "fields": { - "keyword": { - "ignore_above": 256, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, "type": "keyword" } }, - "type": "text" + "type": "object" + } + } + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" }, "version": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" } } }, @@ -36,109 +65,71 @@ "policy": { "properties": { "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" } - } + }, + "type": "object" } } }, "event": { "properties": { "created": { - "type": "long" + "type": "date" } } }, "host": { "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, "hostname": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "ip": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "type": "ip" }, "mac": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "os": { "properties": { "full": { "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" + "text": { + "norms": false, + "type": "text" } }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "name": { "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" + "text": { + "norms": false, + "type": "text" } }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "variant": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "version": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" } } } @@ -146,11 +137,16 @@ } } }, + "order": 1, "settings": { "index": { - "number_of_replicas": "1", - "number_of_shards": "1" + "mapping": { + "total_fields": { + "limit": 10000 + } + }, + "refresh_interval": "5s" } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json index 2046f46db9f53..d3617dc236375 100644 --- a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json +++ b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/data.json @@ -2,7 +2,7 @@ "type": "doc", "value": { "id": "3KVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579881969541, "agent": { @@ -51,7 +51,7 @@ "type": "doc", "value": { "id": "3aVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579881969541, "agent": { @@ -99,7 +99,7 @@ "type": "doc", "value": { "id": "3qVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579881969541, "agent": { @@ -145,7 +145,7 @@ "type": "doc", "value": { "id": "36VN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579878369541, "agent": { @@ -194,7 +194,7 @@ "type": "doc", "value": { "id": "4KVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579878369541, "agent": { @@ -241,7 +241,7 @@ "type": "doc", "value": { "id": "4aVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579878369541, "agent": { @@ -288,7 +288,7 @@ "type": "doc", "value": { "id": "4qVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579874769541, "agent": { @@ -336,7 +336,7 @@ "type": "doc", "value": { "id": "46VN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579874769541, "agent": { @@ -383,7 +383,7 @@ "type": "doc", "value": { "id": "5KVN2G8BYQH1gtPUuYk7", - "index": "endpoint-agent-1", + "index": "metrics-endpoint-default-1", "source": { "@timestamp": 1579874769541, "agent": { diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json index c9a6c183f0489..f9d5de0d0a94c 100644 --- a/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json +++ b/x-pack/test/functional/es_archives/endpoint/metadata/api_feature/mappings.json @@ -1,50 +1,62 @@ { "type": "index", "value": { - "aliases": { - }, - "index": "endpoint-agent-1", + "aliases": {}, + "index": "metrics-endpoint-default-1", "mappings": { + "_meta": { + "version": "1.5.0-dev" + }, + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], "properties": { "@timestamp": { - "type": "long" + "type": "date" }, "elastic": { "properties": { "agent": { "properties": { "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" } - } + }, + "type": "object" } } }, "agent": { "properties": { "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" }, "version": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" } } }, @@ -53,109 +65,71 @@ "policy": { "properties": { "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" } - } + }, + "type": "object" } } }, "event": { "properties": { "created": { - "type": "long" + "type": "date" } } }, "host": { "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, "hostname": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "id": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "ip": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "type": "ip" }, "mac": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "os": { "properties": { "full": { "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" + "text": { + "norms": false, + "type": "text" } }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "name": { "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" + "text": { + "norms": false, + "type": "text" } }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "variant": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" }, "version": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" + "ignore_above": 1024, + "type": "keyword" } } } @@ -163,10 +137,15 @@ } } }, + "order": 1, "settings": { "index": { - "number_of_replicas": "1", - "number_of_shards": "1" + "mapping": { + "total_fields": { + "limit": 10000 + } + }, + "refresh_interval": "5s" } } } diff --git a/x-pack/test/functional/services/uptime/settings.ts b/x-pack/test/functional/services/uptime/settings.ts index a64d39cd62a6d..14cab368b766a 100644 --- a/x-pack/test/functional/services/uptime/settings.ts +++ b/x-pack/test/functional/services/uptime/settings.ts @@ -10,20 +10,41 @@ export function UptimeSettingsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const changeInputField = async (text: string, field: string) => { + const input = await testSubjects.find(field, 5000); + await input.clearValueWithKeyboard(); + await input.type(text); + }; + return { go: async () => { await testSubjects.click('settings-page-link', 5000); }, + changeHeartbeatIndicesInput: async (text: string) => { - const input = await testSubjects.find('heartbeat-indices-input-loaded', 5000); - await input.clearValueWithKeyboard(); - await input.type(text); + await changeInputField(text, 'heartbeat-indices-input-loaded'); + }, + changeErrorThresholdInput: async (text: string) => { + await changeInputField(text, 'error-state-threshold-input-loaded'); + }, + changeWarningThresholdInput: async (text: string) => { + await changeInputField(text, 'warning-state-threshold-input-loaded'); }, loadFields: async () => { - const input = await testSubjects.find('heartbeat-indices-input-loaded', 5000); - const heartbeatIndices = await input.getAttribute('value'); + const indInput = await testSubjects.find('heartbeat-indices-input-loaded', 5000); + const errorInput = await testSubjects.find('error-state-threshold-input-loaded', 5000); + const warningInput = await testSubjects.find('warning-state-threshold-input-loaded', 5000); + const heartbeatIndices = await indInput.getAttribute('value'); + const errorThreshold = await errorInput.getAttribute('value'); + const warningThreshold = await warningInput.getAttribute('value'); - return { heartbeatIndices }; + return { + heartbeatIndices, + certificatesThresholds: { + errorState: errorThreshold, + warningState: warningThreshold, + }, + }; }, applyButtonIsDisabled: async () => { return !!(await (await testSubjects.find('apply-settings-button')).getAttribute('disabled')); diff --git a/x-pack/test/functional_endpoint/apps/endpoint/host_list.ts b/x-pack/test/functional_endpoint/apps/endpoint/host_list.ts index 2e204775808c9..9a4ffecf85d52 100644 --- a/x-pack/test/functional_endpoint/apps/endpoint/host_list.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/host_list.ts @@ -167,7 +167,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '', '0', '00000000-0000-0000-0000-000000000000', - 'active', + 'Successful', '10.101.149.262606:a000:ffc0:39:11ef:37b9:3371:578c', 'rezzani-7.example.com', '6.8.0', diff --git a/x-pack/test/functional_endpoint/config.ts b/x-pack/test/functional_endpoint/config.ts index 37bf57b67b47e..6ae78ab9d48ac 100644 --- a/x-pack/test/functional_endpoint/config.ts +++ b/x-pack/test/functional_endpoint/config.ts @@ -30,6 +30,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), '--xpack.endpoint.enabled=true', '--xpack.ingestManager.enabled=true', + '--xpack.ingestManager.epm.enabled=true', '--xpack.ingestManager.fleet.enabled=true', ], }, diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts index eccbd4fb7f90b..1b6db3a9a31b4 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts @@ -50,7 +50,7 @@ export class EventLogFixturePlugin core.savedObjects.registerType({ name: 'event_log_test', hidden: false, - namespaceAgnostic: true, + namespaceType: 'agnostic', mappings: { properties: {}, }, diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index e9aab9b47535f..df1d7e789507b 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "types": [ "mocha", - "node" + "node", + "flot" ] }, "include": [ diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index a6c94ff74620e..55f697fb4f5ec 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -40,7 +40,8 @@ }, "types": [ "node", - "jest" + "jest", + "flot" ] } } diff --git a/yarn.lock b/yarn.lock index 19d28aea1acfd..45540cd2675b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1220,35 +1220,34 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@elastic/apm-rum-core@^4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-4.7.0.tgz#b00b58bf7380f2e36652e5333e3ca97608986e40" - integrity sha512-/lTZWfA3ces3qoKCx72Sc+w43lZkyktaQlbYoYO86h3tNX7tScc/7YBBHI9oxKMcXweqkKOcpnwNZFy71bb86w== +"@elastic/apm-rum-core@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.2.0.tgz#2ed30dc226c9b5779532ab2e6065a155587bcea4" + integrity sha512-3ti2dhrqfxjHFXgArQI/sVAG2AgZH0kB1nx+2WjLpuAh8gGS4R772M5VXcWcGQb8UW9jrANwwbW2hT2GKv+uOA== dependencies: error-stack-parser "^1.3.5" - es6-promise "^4.2.8" opentracing "^0.14.3" - uuid "^3.1.0" + promise-polyfill "^8.1.3" -"@elastic/apm-rum-react@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-0.3.2.tgz#134634643e15ebcf97b6f17b2c74a50afdbe1c64" - integrity sha512-hU1srW9noygppyrLmipulu30c+LWEie8V/dQjEqLYMx2mRZRwNIue3midYgWa6qrWqgYZhwpAtWrWcXc+AWk2Q== +"@elastic/apm-rum-react@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.1.1.tgz#3a2ba91efc9260da55ef6c31cce642a476eba828" + integrity sha512-ZMixw+82VbZIDBnz0dj5Fo4PZ7pnXlLjAA7XTi3AtSIEjpsZa7YIuCFzJdrgb/nOq7MOFkODkFPTiIYAM/yCqg== dependencies: - "@elastic/apm-rum" "^4.6.0" + "@elastic/apm-rum" "^5.1.1" hoist-non-react-statics "^3.3.0" -"@elastic/apm-rum@^4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-4.6.0.tgz#e2ac560dd4a4761c0e9b08c301418b1d4063bdd2" - integrity sha512-hsqvyTm5rT6lKgV06wvm8ID9aMsuJyw8wIOPjRwKmvzlTjayabxKTcr50lJJV8jY9OWfDkqymIqpHyCEChQAHQ== +"@elastic/apm-rum@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.1.1.tgz#02d29fa606e66f9a3a88f629d468b02457b1b183" + integrity sha512-A0O/0ZffcHm1taLuXyFoUmV2+aARr+9+xbmsEDIExLV0yKaNlaLl3UaZrodSOZ1ijlCEsRRS+y2i0md93YKQTA== dependencies: - "@elastic/apm-rum-core" "^4.7.0" + "@elastic/apm-rum-core" "^5.2.0" -"@elastic/charts@18.2.2": - version "18.2.2" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.2.2.tgz#f59d6ee597d553d193314d8598561c65da787e8d" - integrity sha512-ss8AqLj9wHa2C+9ULUKbXw8ZCQmEjLuaVU5AkqE2j3hOVtAN75HO2p7nMIsxcSldfmqy+4jSptybJLNAfizegQ== +"@elastic/charts@18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.3.0.tgz#cbdeec1860af274edc7a5f5b9dd26ec48c64bb64" + integrity sha512-4kSlSwdDRsVKVX8vRUkwxOu1IT6WIepgLnP0OZT7cFjgrC1SV/16c3YLw2NZDaVe0M/H4rpeNWW30VyrzZVhyw== dependencies: classnames "^2.2.6" d3-array "^1.2.4" @@ -2567,6 +2566,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@sindresorhus/is@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-2.1.0.tgz#6ad4ca610f696098e92954ab431ff83bea0ce13f" + integrity sha512-lXKXfypKo644k4Da4yXkPCrwcvn6SlUW2X2zFbuflKHNjf0w9htru01bo26uMhleMXsDmnZ12eJLdrAZa9MANg== + "@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393" @@ -3473,6 +3477,13 @@ dependencies: defer-to-connect "^1.0.1" +"@szmarczak/http-timer@^4.0.0": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" + integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== + dependencies: + defer-to-connect "^2.0.0" + "@testim/chrome-version@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.0.7.tgz#0cd915785ec4190f08a3a6acc9b61fc38fb5f1a9" @@ -3721,6 +3732,16 @@ resolved "https://registry.yarnpkg.com/@types/browserslist-useragent/-/browserslist-useragent-3.0.0.tgz#d425c9818182ce71ce53866798cee9c7d41d6e53" integrity sha512-ZBvKzg3yyWNYEkwxAzdmUzp27sFvw+1m080/+2lwrt+eltNefn1f4fnpMyrjOla31p8zLleCYqQXw+3EETfn0w== +"@types/cacheable-request@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" + integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" + "@types/caseless@*": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" @@ -4090,6 +4111,11 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/http-cache-semantics@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" + integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== + "@types/indent-string@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/indent-string/-/indent-string-3.0.0.tgz#9ebb391ceda548926f5819ad16405349641b999f" @@ -4221,6 +4247,13 @@ dependencies: "@types/node" "*" +"@types/keyv@*", "@types/keyv@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" + integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== + dependencies: + "@types/node" "*" + "@types/license-checker@15.0.0": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/license-checker/-/license-checker-15.0.0.tgz#685d69e2cf61ffd862320434601f51c85e28bba1" @@ -4692,6 +4725,13 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/responselike@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + "@types/retry@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" @@ -4707,10 +4747,10 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA== -"@types/selenium-webdriver@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.5.tgz#23041a4948c82daf2df9836e4d2358fec10d3e24" - integrity sha512-ma1aL1znI3ptEbSQgbywgadrRCJouPIACSfOl/bPwu/TPNSyyE/+o9jZ6+bpDVTtIdksZuVKpq4SR1ip3DRduw== +"@types/selenium-webdriver@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.9.tgz#12621e55b2ef8f6c98bd17fe23fa720c6cba16bd" + integrity sha512-HopIwBE7GUXsscmt/J0DhnFXLSmO04AfxT6b8HAprknwka7pqEWquWDMXxCjd+NUHK9MkCe1SDKKsMiNmCItbQ== "@types/semver@^5.5.0": version "5.5.0" @@ -8176,6 +8216,14 @@ cache-loader@^4.1.0: neo-async "^2.6.1" schema-utils "^2.0.0" +cacheable-lookup@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-2.0.1.tgz#87be64a18b925234875e10a9bb1ebca4adce6b38" + integrity sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg== + dependencies: + "@types/keyv" "^3.1.1" + keyv "^4.0.0" + cacheable-request@^2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" @@ -8202,6 +8250,19 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" +cacheable-request@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" + integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^2.0.0" + cachedir@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" @@ -10812,6 +10873,13 @@ decompress-response@^4.2.0: dependencies: mimic-response "^2.0.0" +decompress-response@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-5.0.0.tgz#7849396e80e3d1eba8cb2f75ef4930f76461cb0f" + integrity sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw== + dependencies: + mimic-response "^2.0.0" + decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" @@ -10968,6 +11036,11 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +defer-to-connect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" + integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== + define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -12205,11 +12278,6 @@ es6-promise@^4.2.5: resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f" integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q== -es6-promise@^4.2.8: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - es6-promisify@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" @@ -14926,6 +14994,27 @@ got@5.6.0: unzip-response "^1.0.0" url-parse-lax "^1.0.0" +got@^10.7.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/got/-/got-10.7.0.tgz#62889dbcd6cca32cd6a154cc2d0c6895121d091f" + integrity sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg== + dependencies: + "@sindresorhus/is" "^2.0.0" + "@szmarczak/http-timer" "^4.0.0" + "@types/cacheable-request" "^6.0.1" + cacheable-lookup "^2.0.0" + cacheable-request "^7.0.1" + decompress-response "^5.0.0" + duplexer3 "^0.1.4" + get-stream "^5.0.0" + lowercase-keys "^2.0.0" + mimic-response "^2.1.0" + p-cancelable "^2.0.0" + p-event "^4.0.0" + responselike "^2.0.0" + to-readable-stream "^2.0.0" + type-fest "^0.10.0" + got@^3.2.0: version "3.3.1" resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca" @@ -16318,6 +16407,11 @@ ieee754@^1.1.12, ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== +if-async@^3.7.4: + version "3.7.4" + resolved "https://registry.yarnpkg.com/if-async/-/if-async-3.7.4.tgz#55868deb0093d3c67bf7166e745353fb9bcb21a2" + integrity sha1-VYaN6wCT08Z79xZudFNT+5vLIaI= + iferr@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" @@ -17102,6 +17196,11 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.0.0.tgz#038c31b774709641bda678b1f06a4e3227c10b3e" integrity sha512-elzyIdM7iKoFHzcrndIqjYomImhxrFRnGP3galODoII4TB9gI7mZ+FnlLQmmjf27SxHS2gKEeyhX5/+YRS6H9g== +is-generator-function@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== + is-glob@4.0.0, is-glob@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" @@ -18414,6 +18513,11 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a" @@ -18649,10 +18753,10 @@ jsx-to-string@^1.4.0: json-stringify-pretty-compact "^1.0.1" react "^0.14.0" -jszip@^3.1.5: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d" - integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== +jszip@^3.2.2: + version "3.3.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.3.0.tgz#29d72c21a54990fa885b11fc843db320640d5271" + integrity sha512-EJ9k766htB1ZWnsV5ZMDkKLgA+201r/ouFF8R2OigVjVdcm2rurcBrrdXaeqBJbqnUVMko512PYmlncBKE1Huw== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -18832,6 +18936,13 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" +keyv@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.0.tgz#2d1dab694926b2d427e4c74804a10850be44c12f" + integrity sha512-U7ioE8AimvRVLfw4LffyOIRhL2xVgmE8T22L6i0BucSnBUyv4w+I7VN/zVZwRKHOI6ZRUcdMdWHQ8KSUvGpEog== + dependencies: + json-buffer "3.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -20472,6 +20583,11 @@ mimic-response@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.0.0.tgz#996a51c60adf12cb8a87d7fb8ef24c2f3d5ebb46" integrity sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ== +mimic-response@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + mimos@4.x.x: version "4.0.0" resolved "https://registry.yarnpkg.com/mimos/-/mimos-4.0.0.tgz#76e3d27128431cb6482fd15b20475719ad626a5a" @@ -20900,6 +21016,17 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" +ms-chromium-edge-driver@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/ms-chromium-edge-driver/-/ms-chromium-edge-driver-0.2.3.tgz#74effa9280c112a447d8dd4d4c1589fce398a5b6" + integrity sha512-dgGxRdYyz69yhAdJk4BGFY4o5TnKe+LOceTnRQMIl5Qww1pL+1meUuoAIAPVRd5V7kB7ZfgYQNxtxQj/fVUmUA== + dependencies: + extract-zip "^2.0.0" + got "^10.7.0" + lodash "^4.17.15" + regedit "^3.0.3" + util "^0.12.2" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -22241,6 +22368,11 @@ p-cancelable@^1.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== +p-cancelable@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" + integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -22253,7 +22385,7 @@ p-each-series@^1.0.0: dependencies: p-reduce "^1.0.0" -p-event@^4.1.0: +p-event@^4.0.0, p-event@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.1.0.tgz#e92bb866d7e8e5b732293b1c8269d38e9982bf8e" integrity sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA== @@ -23439,6 +23571,11 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= +promise-polyfill@^8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116" + integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g== + promise.prototype.finally@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.0.tgz#66f161b1643636e50e7cf201dc1b84a857f3864e" @@ -25038,7 +25175,7 @@ read-pkg@^5.1.1, read-pkg@^5.2.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@1.0: +readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0": version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= @@ -25357,6 +25494,16 @@ refractor@^2.4.1: parse-entities "^1.1.2" prismjs "~1.16.0" +regedit@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/regedit/-/regedit-3.0.3.tgz#0c2188e15f670de7d5740c5cea9bbebe99497749" + integrity sha512-SpHmMKOtiEYx0MiRRC48apBsmThoZ4svZNsYoK8leHd5bdUHV1nYb8pk8gh6Moou7/S9EDi1QsjBTpyXVQrPuQ== + dependencies: + debug "^4.1.0" + if-async "^3.7.4" + stream-slicer "0.0.6" + through2 "^0.6.3" + regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -26096,6 +26243,13 @@ responselike@1.0.2, responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" +responselike@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" + integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== + dependencies: + lowercase-keys "^2.0.0" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -26654,15 +26808,14 @@ select@^1.1.2: resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= -selenium-webdriver@^4.0.0-alpha.5: - version "4.0.0-alpha.5" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.5.tgz#e4683b3dbf827d70df09a7e43bf02ebad20fa7c1" - integrity sha512-hktl3DSrhzM59yLhWzDGHIX9o56DvA+cVK7Dw6FcJR6qQ4CGzkaHeXQPcdrslkWMTeq0Ci9AmCxq0EMOvm2Rkg== +selenium-webdriver@^4.0.0-alpha.7: + version "4.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.7.tgz#e3879d8457fd7ad8e4424094b7dc0540d99e6797" + integrity sha512-D4qnTsyTr91jT8f7MfN+OwY0IlU5+5FmlO5xlgRUV6hDEV8JyYx2NerdTEqDDkNq7RZDYc4VoPALk8l578RBHw== dependencies: - jszip "^3.1.5" - rimraf "^2.6.3" + jszip "^3.2.2" + rimraf "^2.7.1" tmp "0.0.30" - xml2js "^0.4.19" selfsigned@^1.10.7: version "1.10.7" @@ -27826,6 +27979,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= +stream-slicer@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/stream-slicer/-/stream-slicer-0.0.6.tgz#f86b2ac5c2440b7a0a87b71f33665c0788046138" + integrity sha1-+GsqxcJEC3oKh7cfM2ZcB4gEYTg= + stream-spigot@~2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/stream-spigot/-/stream-spigot-2.1.2.tgz#7de145e819f8dd0db45090d13dcf73a8ed3cc035" @@ -28844,6 +29002,14 @@ through2@2.X, through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: readable-stream "^2.1.5" xtend "~4.0.1" +through2@^0.6.3: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg= + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + through2@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" @@ -29084,6 +29250,11 @@ to-readable-stream@^1.0.0: resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== +to-readable-stream@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-2.1.0.tgz#82880316121bea662cdc226adb30addb50cb06e8" + integrity sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w== + to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" @@ -29874,6 +30045,11 @@ type-detect@^1.0.0: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" integrity sha1-diIXzAbbJY7EiQihKY6LlRIejqI= +type-fest@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.10.0.tgz#7f06b2b9fbfc581068d1341ffabd0349ceafc642" + integrity sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw== + type-fest@^0.3.0, type-fest@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -30604,6 +30780,16 @@ util@^0.11.0: dependencies: inherits "2.0.3" +util@^0.12.2: + version "0.12.2" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.2.tgz#54adb634c9e7c748707af2bf5a8c7ab640cbba2b" + integrity sha512-XE+MkWQvglYa+IOfBt5UFG93EmncEMP23UqpgDvVZVFBPxwmkK10QRp6pgU4xICPnWRf/t0zPv4noYSUq9gqUQ== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + safe-buffer "^5.1.2" + utila@^0.4.0, utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" @@ -30674,7 +30860,7 @@ utils-regex-from-string@^1.0.0: regex-regex "^1.0.0" validate.io-string-primitive "^1.0.0" -uuid@3.3.2, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2: +uuid@3.3.2, uuid@^3.0.1, uuid@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== @@ -30689,7 +30875,7 @@ uuid@^3.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g== -uuid@^3.3.3: +uuid@^3.1.0, uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -32085,14 +32271,6 @@ xml-parse-from-string@^1.0.0: resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28" integrity sha1-qQKekp09vN7RafPG4oI42VpdWig= -xml2js@^0.4.19, xml2js@^0.4.5: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - xml2js@^0.4.22: version "0.4.22" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.22.tgz#4fa2d846ec803237de86f30aa9b5f70b6600de02" @@ -32102,6 +32280,14 @@ xml2js@^0.4.22: util.promisify "~1.0.0" xmlbuilder "~11.0.0" +xml2js@^0.4.5: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + xml@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" @@ -32154,16 +32340,16 @@ xregexp@4.2.4: dependencies: "@babel/runtime-corejs2" "^7.2.0" +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= -xtend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - xxhashjs@^0.2.1: version "0.2.2" resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8"