From 3b3327dbc3c3041c9681e0cd86bd31cf411dc460 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 8 Feb 2021 10:19:54 +0100 Subject: [PATCH 001/100] Migrate most plugins to synchronous lifecycle (#89562) * first pass * migrate more plugins * migrate yet more plugins * more oss plugins * fix test file * change Plugin signature on the client-side too * fix test types * migrate OSS client-side plugins * migrate OSS client-side test plugins * migrate xpack client-side plugins * revert fix attempt on fleet plugin * fix presentation start signature * fix yet another signature * add warnings for server-side async plugins in dev mode * remove unused import * fix isPromise * Add client-side deprecations * update migration examples * update generated doc * fix xpack unit tests * nit * (will be reverted) explicitly await for license to be ready in the auth hook * Revert "(will be reverted) explicitly await for license to be ready in the auth hook" This reverts commit fdf73feb * restore await on on promise contracts * Revert "(will be reverted) explicitly await for license to be ready in the auth hook" This reverts commit fdf73feb * Revert "restore await on on promise contracts" This reverts commit c5f2fe51 * add delay before starting tests in FTR * update deprecation ts doc * add explicit contract for monitoring setup * migrate monitoring plugin to sync * change plugin timeout to 10sec * use delay instead of silence --- ...migrating-legacy-plugins-examples.asciidoc | 12 +- .../kibana-plugin-core-public.asyncplugin.md | 27 ++++ ...na-plugin-core-public.asyncplugin.setup.md | 23 +++ ...na-plugin-core-public.asyncplugin.start.md | 23 +++ ...ana-plugin-core-public.asyncplugin.stop.md | 15 ++ .../core/public/kibana-plugin-core-public.md | 1 + .../kibana-plugin-core-public.plugin.setup.md | 4 +- .../kibana-plugin-core-public.plugin.start.md | 4 +- ...na-plugin-core-public.plugininitializer.md | 2 +- .../kibana-plugin-core-server.asyncplugin.md | 27 ++++ ...na-plugin-core-server.asyncplugin.setup.md | 23 +++ ...na-plugin-core-server.asyncplugin.start.md | 23 +++ ...ana-plugin-core-server.asyncplugin.stop.md | 15 ++ .../core/server/kibana-plugin-core-server.md | 1 + .../kibana-plugin-core-server.plugin.setup.md | 4 +- .../kibana-plugin-core-server.plugin.start.md | 4 +- ...na-plugin-core-server.plugininitializer.md | 2 +- packages/kbn-std/src/index.ts | 2 +- packages/kbn-std/src/promise.test.ts | 29 +++- packages/kbn-std/src/promise.ts | 4 + .../kbn-test/src/functional_tests/tasks.js | 6 + src/core/public/index.ts | 9 +- src/core/public/mocks.ts | 8 +- src/core/public/plugins/index.ts | 2 +- src/core/public/plugins/plugin.test.ts | 12 +- src/core/public/plugins/plugin.ts | 57 +++++-- .../plugins/plugins_service.test.mocks.ts | 7 +- .../public/plugins/plugins_service.test.ts | 144 +++++++++++++++-- src/core/public/plugins/plugins_service.ts | 63 +++++--- src/core/public/public.api.md | 16 +- src/core/server/index.ts | 1 + .../integration_tests/plugins_service.test.ts | 4 +- src/core/server/plugins/plugin.test.ts | 36 ++--- src/core/server/plugins/plugin.ts | 27 +++- .../server/plugins/plugins_system.test.ts | 152 +++++++++++++++--- src/core/server/plugins/plugins_system.ts | 56 +++++-- src/core/server/plugins/types.ts | 23 ++- src/core/server/server.api.md | 24 ++- src/plugins/apm_oss/server/plugin.ts | 7 +- src/plugins/console/server/plugin.ts | 7 +- src/plugins/inspector/public/plugin.tsx | 2 +- src/plugins/legacy_export/server/plugin.ts | 7 +- src/plugins/maps_legacy/server/index.ts | 8 +- .../presentation_util/public/plugin.ts | 4 +- src/plugins/region_map/public/plugin.ts | 2 +- .../saved_objects_management/server/plugin.ts | 4 +- src/plugins/share/server/plugin.ts | 2 +- src/plugins/tile_map/public/plugin.ts | 2 +- src/plugins/usage_collection/server/plugin.ts | 12 +- src/plugins/vis_type_table/public/plugin.ts | 5 +- src/plugins/vis_type_timelion/server/index.ts | 4 +- .../vis_type_timelion/server/plugin.ts | 15 +- .../vis_type_timeseries/public/plugin.ts | 4 +- src/plugins/vis_type_vega/public/plugin.ts | 4 +- src/plugins/vis_type_vislib/public/plugin.ts | 2 +- src/plugins/vis_type_xy/public/plugin.ts | 2 +- src/plugins/visualize/public/plugin.ts | 2 +- .../plugins/app_link_test/public/plugin.ts | 2 +- .../plugins/core_plugin_b/public/plugin.tsx | 2 +- x-pack/plugins/actions/server/plugin.ts | 45 +++--- x-pack/plugins/apm/server/plugin.ts | 7 +- .../plugins/beats_management/server/plugin.ts | 18 ++- x-pack/plugins/canvas/server/plugin.ts | 7 +- x-pack/plugins/case/server/plugin.ts | 9 +- x-pack/plugins/cloud/public/plugin.ts | 2 +- x-pack/plugins/cloud/server/plugin.ts | 17 +- x-pack/plugins/code/server/plugin.ts | 10 +- .../encrypted_saved_objects/server/index.ts | 4 +- .../server/plugin.test.ts | 18 +-- .../encrypted_saved_objects/server/plugin.ts | 23 ++- .../enterprise_search/server/plugin.ts | 12 +- x-pack/plugins/event_log/server/plugin.ts | 22 +-- x-pack/plugins/event_log/server/types.ts | 2 - x-pack/plugins/features/server/index.ts | 4 +- x-pack/plugins/features/server/plugin.test.ts | 12 +- x-pack/plugins/features/server/plugin.ts | 9 +- x-pack/plugins/fleet/public/plugin.ts | 2 +- x-pack/plugins/fleet/server/plugin.ts | 4 +- x-pack/plugins/global_search/server/plugin.ts | 11 +- x-pack/plugins/graph/server/plugin.ts | 2 +- .../server/plugin.ts | 13 +- x-pack/plugins/infra/server/plugin.ts | 18 +-- x-pack/plugins/licensing/public/plugin.ts | 2 +- x-pack/plugins/licensing/server/plugin.ts | 12 +- x-pack/plugins/lists/server/create_config.ts | 18 --- x-pack/plugins/lists/server/plugin.ts | 8 +- x-pack/plugins/maps/server/plugin.ts | 8 +- x-pack/plugins/monitoring/public/plugin.ts | 2 +- x-pack/plugins/monitoring/server/index.ts | 6 +- .../plugins/monitoring/server/plugin.test.ts | 56 +------ x-pack/plugins/monitoring/server/plugin.ts | 19 +-- x-pack/plugins/monitoring/server/types.ts | 4 + x-pack/plugins/observability/server/plugin.ts | 7 +- .../plugins/osquery/server/create_config.ts | 8 +- x-pack/plugins/osquery/server/plugin.ts | 7 +- x-pack/plugins/painless_lab/server/plugin.ts | 2 +- .../plugins/remote_clusters/server/plugin.ts | 11 +- .../plugins/searchprofiler/server/plugin.ts | 2 +- x-pack/plugins/security/server/index.ts | 4 +- x-pack/plugins/security/server/plugin.test.ts | 6 +- x-pack/plugins/security/server/plugin.ts | 10 +- .../security_solution/server/config.ts | 9 +- .../security_solution/server/plugin.ts | 15 +- .../plugins/snapshot_restore/server/plugin.ts | 10 +- x-pack/plugins/spaces/server/index.ts | 4 +- x-pack/plugins/spaces/server/plugin.test.ts | 12 +- x-pack/plugins/spaces/server/plugin.ts | 4 +- x-pack/plugins/stack_alerts/server/plugin.ts | 9 +- .../task_manager/server/plugin.test.ts | 2 +- x-pack/plugins/task_manager/server/plugin.ts | 12 +- .../triggers_actions_ui/server/plugin.ts | 4 +- x-pack/plugins/uptime/public/apps/plugin.ts | 5 +- x-pack/plugins/watcher/server/plugin.ts | 2 +- x-pack/plugins/xpack_legacy/server/plugin.ts | 8 +- 114 files changed, 1000 insertions(+), 550 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md delete mode 100644 x-pack/plugins/lists/server/create_config.ts diff --git a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc index a033bbd26a1a7..92a624649d3c5 100644 --- a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc +++ b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc @@ -71,22 +71,20 @@ export function plugin(initializerContext: PluginInitializerContext) { *plugins/my_plugin/(public|server)/plugin.ts* [source,typescript] ---- -import type { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext, PluginName } from 'kibana/server'; import type { MyPluginConfig } from './config'; export class MyPlugin implements Plugin { - private readonly config$: Observable; + private readonly config: MyPluginConfig; private readonly log: Logger; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = initializerContext.logger.get(); - this.config$ = initializerContext.config.create(); + this.config = initializerContext.config.get(); } - public async setup(core: CoreSetup, deps: Record) { - const isEnabled = await this.config$.pipe(first()).toPromise(); + public setup(core: CoreSetup, deps: Record) { + const { someConfigValue } = this.config; } } ---- @@ -96,7 +94,7 @@ Additionally, some plugins need to access the runtime env configuration. [source,typescript] ---- export class MyPlugin implements Plugin { - public async setup(core: CoreSetup, deps: Record) { + public setup(core: CoreSetup, deps: Record) { const { mode: { dev }, packageInfo: { version } } = this.initializerContext.env } ---- diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md new file mode 100644 index 0000000000000..cf315e1fd337e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) + +## AsyncPlugin interface + +> Warning: This API is now obsolete. +> +> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-public.plugin.md) +> + +A plugin with asynchronous lifecycle methods. + +Signature: + +```typescript +export interface AsyncPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-public.asyncplugin.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-public.asyncplugin.start.md) | | +| [stop()](./kibana-plugin-core-public.asyncplugin.stop.md) | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md new file mode 100644 index 0000000000000..54507b44cdd72 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [setup](./kibana-plugin-core-public.asyncplugin.setup.md) + +## AsyncPlugin.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup<TPluginsStart, TStart> | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup | Promise` + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md new file mode 100644 index 0000000000000..f16d3c46bf849 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [start](./kibana-plugin-core-public.asyncplugin.start.md) + +## AsyncPlugin.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +`TStart | Promise` + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md new file mode 100644 index 0000000000000..f809f75783c26 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [stop](./kibana-plugin-core-public.asyncplugin.stop.md) + +## AsyncPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index efd499823ffad..e307b5c9971b0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -39,6 +39,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | | [AppMeta](./kibana-plugin-core-public.appmeta.md) | Input type for meta data for an application.Meta fields include keywords and searchDeepLinks Keywords is an array of string with which to associate the app, must include at least one unique string as an array. searchDeepLinks is an array of links that represent secondary in-app locations for the app. | | [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | | +| [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-core-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-core-public.chromebrand.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md index 7fa05588a3301..232851cd342ce 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Returns: -`TSetup | Promise` +`TSetup` diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md index 0d3c19a8217a6..ec5ed211a9d2b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): TStart; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`TStart` diff --git a/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md b/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md index 1fcc2999dfd2e..b7c3e11e492bd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `public` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md new file mode 100644 index 0000000000000..1ad1d87220b74 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) + +## AsyncPlugin interface + +> Warning: This API is now obsolete. +> +> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-server.plugin.md) +> + +A plugin with asynchronous lifecycle methods. + +Signature: + +```typescript +export interface AsyncPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-server.asyncplugin.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-server.asyncplugin.start.md) | | +| [stop()](./kibana-plugin-core-server.asyncplugin.stop.md) | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md new file mode 100644 index 0000000000000..1d033b7b88b05 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [setup](./kibana-plugin-core-server.asyncplugin.setup.md) + +## AsyncPlugin.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup | Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md new file mode 100644 index 0000000000000..3cce90f01603b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [start](./kibana-plugin-core-server.asyncplugin.start.md) + +## AsyncPlugin.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +`TStart | Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md new file mode 100644 index 0000000000000..9272fc2c4eba0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [stop](./kibana-plugin-core-server.asyncplugin.stop.md) + +## AsyncPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 82f4a285409c9..5fe5eda7a8172 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -49,6 +49,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppCategory](./kibana-plugin-core-server.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav | | [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) | | | [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) | | +| [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Authenticated](./kibana-plugin-core-server.authenticated.md) | | | [AuthNotHandled](./kibana-plugin-core-server.authnothandled.md) | | | [AuthRedirected](./kibana-plugin-core-server.authredirected.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md index b4e6623098736..a8b0aae28d251 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; Returns: -`TSetup | Promise` +`TSetup` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md index 03e889a018b6f..851f84474fe11 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): TStart; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`TStart` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md index 839eabff29a18..fe55e131065dd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `server` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; ``` diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts index f3d9e0f77fa19..d79594c97cec7 100644 --- a/packages/kbn-std/src/index.ts +++ b/packages/kbn-std/src/index.ts @@ -12,7 +12,7 @@ export { get } from './get'; export { mapToObject } from './map_to_object'; export { merge } from './merge'; export { pick } from './pick'; -export { withTimeout } from './promise'; +export { withTimeout, isPromise } from './promise'; export { isRelativeUrl, modifyUrl, getUrlOrigin, URLMeaningfulParts } from './url'; export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; diff --git a/packages/kbn-std/src/promise.test.ts b/packages/kbn-std/src/promise.test.ts index 61197a2a8bf70..f7c119acd0c7a 100644 --- a/packages/kbn-std/src/promise.test.ts +++ b/packages/kbn-std/src/promise.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from './promise'; +import { withTimeout, isPromise } from './promise'; const delay = (ms: number, resolveValue?: any) => new Promise((resolve) => setTimeout(resolve, ms, resolveValue)); @@ -50,3 +50,30 @@ describe('withTimeout', () => { ).rejects.toMatchInlineSnapshot(`[Error: from-promise]`); }); }); + +describe('isPromise', () => { + it('returns true when arg is a Promise', () => { + expect(isPromise(Promise.resolve('foo'))).toEqual(true); + expect(isPromise(Promise.reject('foo').catch(() => undefined))).toEqual(true); + }); + + it('returns false when arg is not a Promise', () => { + expect(isPromise(12)).toEqual(false); + expect(isPromise('foo')).toEqual(false); + expect(isPromise({ hello: 'dolly' })).toEqual(false); + expect(isPromise([1, 2, 3])).toEqual(false); + }); + + it('returns false for objects with a non-function `then` property', () => { + expect(isPromise({ then: 'bar' })).toEqual(false); + }); + + it('returns false for null and undefined', () => { + expect(isPromise(null)).toEqual(false); + expect(isPromise(undefined)).toEqual(false); + }); + + it('returns true for Promise-Like objects', () => { + expect(isPromise({ then: () => 12 })).toEqual(true); + }); +}); diff --git a/packages/kbn-std/src/promise.ts b/packages/kbn-std/src/promise.ts index ce4e50bf9b2ac..9d8f7703c026d 100644 --- a/packages/kbn-std/src/promise.ts +++ b/packages/kbn-std/src/promise.ts @@ -20,3 +20,7 @@ export function withTimeout({ new Promise((resolve, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout)), ]) as Promise; } + +export function isPromise(maybePromise: T | Promise): maybePromise is Promise { + return maybePromise ? typeof (maybePromise as Promise).then === 'function' : false; +} diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 099963545a2dc..02c55b6af91dc 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -95,6 +95,8 @@ export async function runTests(options) { try { es = await runElasticsearch({ config, options: opts }); await runKibanaServer({ procs, config, options: opts }); + // workaround until https://github.com/elastic/kibana/issues/89828 is addressed + await delay(5000); await runFtr({ configPath, options: opts }); } finally { try { @@ -160,3 +162,7 @@ async function silence(log, milliseconds) { ) .toPromise(); } + +async function delay(ms) { + await new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/core/public/index.ts b/src/core/public/index.ts index afa129adc061f..a1cb036ce38f8 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -53,7 +53,13 @@ import { HttpSetup, HttpStart } from './http'; import { I18nStart } from './i18n'; import { NotificationsSetup, NotificationsStart } from './notifications'; import { OverlayStart } from './overlays'; -import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins'; +import { + Plugin, + AsyncPlugin, + PluginInitializer, + PluginInitializerContext, + PluginOpaqueId, +} from './plugins'; import { UiSettingsState, IUiSettingsClient } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; @@ -304,6 +310,7 @@ export { NotificationsSetup, NotificationsStart, Plugin, + AsyncPlugin, PluginInitializer, PluginInitializerContext, SavedObjectsStart, diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index d208ea76c48fe..e47de84ea12b2 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -110,14 +110,14 @@ function pluginInitializerContextMock(config: any = {}) { return mock; } -function createCoreContext(): CoreContext { +function createCoreContext({ production = false }: { production?: boolean } = {}): CoreContext { return { coreId: Symbol('core context mock'), env: { mode: { - dev: true, - name: 'development', - prod: false, + dev: !production, + name: production ? 'production' : 'development', + prod: production, }, packageInfo: { version: 'version', diff --git a/src/core/public/plugins/index.ts b/src/core/public/plugins/index.ts index 76811d4908d22..be805c6a521ce 100644 --- a/src/core/public/plugins/index.ts +++ b/src/core/public/plugins/index.ts @@ -7,6 +7,6 @@ */ export * from './plugins_service'; -export { Plugin, PluginInitializer } from './plugin'; +export { Plugin, AsyncPlugin, PluginInitializer } from './plugin'; export { PluginInitializerContext } from './plugin_context'; export { PluginOpaqueId } from '../../server/types'; diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index e8e930a5befca..ef919018f120b 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -39,16 +39,16 @@ beforeEach(() => { }); describe('PluginWrapper', () => { - test('`setup` fails if plugin.setup is not a function', async () => { + test('`setup` fails if plugin.setup is not a function', () => { mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any); - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.setup({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."` ); }); - test('`setup` fails if plugin.start is not a function', async () => { + test('`setup` fails if plugin.start is not a function', () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any); - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.setup({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."` ); }); @@ -65,8 +65,8 @@ describe('PluginWrapper', () => { expect(mockPlugin.setup).toHaveBeenCalledWith(context, deps); }); - test('`start` fails if setup is not called first', async () => { - await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + test('`start` fails if setup is not called first', () => { + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be started since it isn't set up."` ); }); diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index af95e831a6472..a08a6cf0b431a 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -8,6 +8,7 @@ import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; +import { isPromise } from '@kbn/std'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; import { read } from './plugin_reader'; @@ -23,6 +24,23 @@ export interface Plugin< TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; +} + +/** + * A plugin with asynchronous lifecycle methods. + * + * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} + * @public + */ +export interface AsyncPlugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; @@ -40,7 +58,11 @@ export type PluginInitializer< TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; +> = ( + core: PluginInitializerContext +) => + | Plugin + | AsyncPlugin; /** * Lightweight wrapper around discovered plugin that is responsible for instantiating @@ -58,7 +80,9 @@ export class PluginWrapper< public readonly configPath: DiscoveredPlugin['configPath']; public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins']; public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins']; - private instance?: Plugin; + private instance?: + | Plugin + | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); @@ -81,10 +105,12 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { - this.instance = await this.createPluginInstance(); - - return await this.instance.setup(setupContext, plugins); + public setup( + setupContext: CoreSetup, + plugins: TPluginsSetup + ): TSetup | Promise { + this.instance = this.createPluginInstance(); + return this.instance.setup(setupContext, plugins); } /** @@ -94,16 +120,21 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: CoreStart, plugins: TPluginsStart) { + public start(startContext: CoreStart, plugins: TPluginsStart) { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - const startContract = await this.instance.start(startContext, plugins); - - this.startDependencies$.next([startContext, plugins, startContract]); - - return startContract; + const startContract = this.instance.start(startContext, plugins); + if (isPromise(startContract)) { + return startContract.then((resolvedContract) => { + this.startDependencies$.next([startContext, plugins, resolvedContract]); + return resolvedContract; + }); + } else { + this.startDependencies$.next([startContext, plugins, startContract]); + return startContract; + } } /** @@ -121,7 +152,7 @@ export class PluginWrapper< this.instance = undefined; } - private async createPluginInstance() { + private createPluginInstance() { const initializer = read(this.name) as PluginInitializer< TSetup, TStart, diff --git a/src/core/public/plugins/plugins_service.test.mocks.ts b/src/core/public/plugins/plugins_service.test.mocks.ts index d44657f9039a3..1f85482569dbc 100644 --- a/src/core/public/plugins/plugins_service.test.mocks.ts +++ b/src/core/public/plugins/plugins_service.test.mocks.ts @@ -7,9 +7,12 @@ */ import { PluginName } from 'kibana/server'; -import { Plugin } from './plugin'; +import { Plugin, AsyncPlugin } from './plugin'; -export type MockedPluginInitializer = jest.Mock>, any>; +export type MockedPluginInitializer = jest.Mock< + Plugin | AsyncPlugin, + any +>; export const mockPluginInitializerProvider: jest.Mock< MockedPluginInitializer, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index a22d48c50247a..e70b78f237d75 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -146,16 +146,16 @@ describe('PluginsService', () => { it('returns dependency tree of symbols', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(` - Map { - Symbol(pluginA) => Array [], - Symbol(pluginB) => Array [ - Symbol(pluginA), - ], - Symbol(pluginC) => Array [ - Symbol(pluginA), - ], - } - `); + Map { + Symbol(pluginA) => Array [], + Symbol(pluginB) => Array [ + Symbol(pluginA), + ], + Symbol(pluginC) => Array [ + Symbol(pluginA), + ], + } + `); }); }); @@ -264,7 +264,7 @@ describe('PluginsService', () => { jest.runAllTimers(); // setup plugins await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); }); @@ -344,7 +344,7 @@ describe('PluginsService', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); }); @@ -366,4 +366,124 @@ describe('PluginsService', () => { expect(pluginCInstance.stop).toHaveBeenCalled(); }); }); + + describe('asynchronous plugins', () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + const runScenario = async ({ + production, + asyncSetup, + asyncStart, + }: { + production: boolean; + asyncSetup: boolean; + asyncStart: boolean; + }) => { + const coreContext = coreMock.createCoreContext({ production }); + + const syncPlugin = { id: 'sync-plugin', plugin: createManifest('sync-plugin') }; + mockPluginInitializers.set( + 'sync-plugin', + jest.fn(() => ({ + setup: jest.fn(() => 'setup-sync'), + start: jest.fn(() => 'start-sync'), + stop: jest.fn(), + })) + ); + + const asyncPlugin = { id: 'async-plugin', plugin: createManifest('async-plugin') }; + mockPluginInitializers.set( + 'async-plugin', + jest.fn(() => ({ + setup: jest.fn(() => (asyncSetup ? Promise.resolve('setup-async') : 'setup-sync')), + start: jest.fn(() => (asyncStart ? Promise.resolve('start-async') : 'start-sync')), + stop: jest.fn(), + })) + ); + + const pluginsService = new PluginsService(coreContext, [syncPlugin, asyncPlugin]); + + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + }; + + it('logs a warning if a plugin returns a promise from its setup contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: false, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its setup contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: true, + asyncStart: false, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs a warning if a plugin returns a promise from its start contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: false, + asyncStart: true, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its start contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: false, + asyncStart: true, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs multiple warnings if both `setup` and `start` return promises', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: true, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + }); }); diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 7a10ce1cdfc77..57fbe4cbecd12 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from '@kbn/std'; +import { withTimeout, isPromise } from '@kbn/std'; import { PluginName, PluginOpaqueId } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; @@ -98,16 +98,29 @@ export class PluginsService implements CoreService ); - const contract = await withTimeout({ - promise: plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); - contracts.set(pluginName, contract); + let contract: unknown; + const contractOrPromise = plugin.setup( + createPluginSetupContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + // eslint-disable-next-line no-console + console.log( + `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } + contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); } @@ -132,14 +145,28 @@ export class PluginsService implements CoreService ); - const contract = await withTimeout({ - promise: plugin.start( - createPluginStartContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + // eslint-disable-next-line no-console + console.log( + `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } + contracts.set(pluginName, contract); } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 75ed9aa5f150f..99579ada8ec58 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -194,6 +194,16 @@ export type AppUpdatableFields = Pick Partial | undefined; +// @public @deprecated +export interface AsyncPlugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + // @public export interface Capabilities { [key: string]: Record>; @@ -990,15 +1000,15 @@ export { PackageInfo } // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart; // (undocumented) stop?(): void; } // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; // @public export interface PluginInitializerContext { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 382a694bd2e41..6f478004c204e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -235,6 +235,7 @@ export { export { DiscoveredPlugin, Plugin, + AsyncPlugin, PluginConfigDescriptor, PluginConfigSchema, PluginInitializer, diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts index dda947972737a..a29fb01fbc009 100644 --- a/src/core/server/plugins/integration_tests/plugins_service.test.ts +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -20,7 +20,7 @@ import { config } from '../plugins_config'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { environmentServiceMock } from '../../environment/environment_service.mock'; import { coreMock } from '../../mocks'; -import { Plugin } from '../types'; +import { AsyncPlugin } from '../types'; import { PluginWrapper } from '../plugin'; describe('PluginsService', () => { @@ -138,7 +138,7 @@ describe('PluginsService', () => { expect(startDependenciesResolved).toBe(false); return pluginStartContract; }, - } as Plugin); + } as AsyncPlugin); jest.doMock( join(pluginPath, 'server'), diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 68fdfdf62c30b..c90d2e804225c 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -100,7 +100,7 @@ test('`constructor` correctly initializes plugin instance', () => { expect(plugin.optionalPlugins).toEqual(['some-optional-dep']); }); -test('`setup` fails if `plugin` initializer is not exported', async () => { +test('`setup` fails if `plugin` initializer is not exported', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -115,14 +115,14 @@ test('`setup` fails if `plugin` initializer is not exported', async () => { ), }); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Plugin "some-plugin-id" does not export "plugin" definition (plugin-without-initializer-path).]` + ).toThrowErrorMatchingInlineSnapshot( + `"Plugin \\"some-plugin-id\\" does not export \\"plugin\\" definition (plugin-without-initializer-path)."` ); }); -test('`setup` fails if plugin initializer is not a function', async () => { +test('`setup` fails if plugin initializer is not a function', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -137,14 +137,14 @@ test('`setup` fails if plugin initializer is not a function', async () => { ), }); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Definition of plugin "some-plugin-id" should be a function (plugin-with-wrong-initializer-path).]` + ).toThrowErrorMatchingInlineSnapshot( + `"Definition of plugin \\"some-plugin-id\\" should be a function (plugin-with-wrong-initializer-path)."` ); }); -test('`setup` fails if initializer does not return object', async () => { +test('`setup` fails if initializer does not return object', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -161,14 +161,14 @@ test('`setup` fails if initializer does not return object', async () => { mockPluginInitializer.mockReturnValue(null); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Initializer for plugin "some-plugin-id" is expected to return plugin instance, but returned "null".]` + ).toThrowErrorMatchingInlineSnapshot( + `"Initializer for plugin \\"some-plugin-id\\" is expected to return plugin instance, but returned \\"null\\"."` ); }); -test('`setup` fails if object returned from initializer does not define `setup` function', async () => { +test('`setup` fails if object returned from initializer does not define `setup` function', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -186,10 +186,10 @@ test('`setup` fails if object returned from initializer does not define `setup` const mockPluginInstance = { run: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Instance of plugin "some-plugin-id" does not define "setup" function.]` + ).toThrowErrorMatchingInlineSnapshot( + `"Instance of plugin \\"some-plugin-id\\" does not define \\"setup\\" function."` ); }); @@ -223,7 +223,7 @@ test('`setup` initializes plugin and calls appropriate lifecycle hook', async () expect(mockPluginInstance.setup).toHaveBeenCalledWith(setupContext, setupDependencies); }); -test('`start` fails if setup is not called first', async () => { +test('`start` fails if setup is not called first', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -238,7 +238,7 @@ test('`start` fails if setup is not called first', async () => { ), }); - await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"some-plugin-id\\" can't be started since it isn't set up."` ); }); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 83b3fb53689a7..ca7f11e28de75 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -10,11 +10,13 @@ import { join } from 'path'; import typeDetect from 'type-detect'; import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; +import { isPromise } from '@kbn/std'; import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../logging'; import { Plugin, + AsyncPlugin, PluginInitializerContext, PluginManifest, PluginInitializer, @@ -49,7 +51,9 @@ export class PluginWrapper< private readonly log: Logger; private readonly initializerContext: PluginInitializerContext; - private instance?: Plugin; + private instance?: + | Plugin + | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); @@ -83,9 +87,11 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { + public setup( + setupContext: CoreSetup, + plugins: TPluginsSetup + ): TSetup | Promise { this.instance = this.createPluginInstance(); - return this.instance.setup(setupContext, plugins); } @@ -96,14 +102,21 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: CoreStart, plugins: TPluginsStart) { + public start(startContext: CoreStart, plugins: TPluginsStart): TStart | Promise { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - const startContract = await this.instance.start(startContext, plugins); - this.startDependencies$.next([startContext, plugins, startContract]); - return startContract; + const startContract = this.instance.start(startContext, plugins); + if (isPromise(startContract)) { + return startContract.then((resolvedContract) => { + this.startDependencies$.next([startContext, plugins, resolvedContract]); + return resolvedContract; + }); + } else { + this.startDependencies$.next([startContext, plugins, startContract]); + return startContract; + } } /** diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 1b5994c40c041..5c38deeb5cf6e 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -25,7 +25,6 @@ import { PluginsSystem } from './plugins_system'; import { coreMock } from '../mocks'; import { Logger } from '../logging'; -const logger = loggingSystemMock.create(); function createPlugin( id: string, { @@ -34,8 +33,8 @@ function createPlugin( server = true, ui = true, }: { required?: string[]; optional?: string[]; server?: boolean; ui?: boolean } = {} -) { - return new PluginWrapper({ +): PluginWrapper { + return new PluginWrapper({ path: 'some-path', manifest: { id, @@ -53,27 +52,27 @@ function createPlugin( }); } +const setupDeps = coreMock.createInternalSetup(); +const startDeps = coreMock.createInternalStart(); + let pluginsSystem: PluginsSystem; -const configService = configServiceMock.create(); -configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); +let configService: ReturnType; +let logger: ReturnType; let env: Env; let coreContext: CoreContext; -const setupDeps = coreMock.createInternalSetup(); -const startDeps = coreMock.createInternalStart(); - beforeEach(() => { + logger = loggingSystemMock.create(); env = Env.createDefault(REPO_ROOT, getEnvOptions()); + configService = configServiceMock.create(); + configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; pluginsSystem = new PluginsSystem(coreContext); }); -afterEach(() => { - jest.clearAllMocks(); -}); - test('can be setup even without plugins', async () => { const pluginsSetup = await pluginsSystem.setupPlugins(setupDeps); @@ -208,7 +207,7 @@ test('correctly orders plugins and returns exposed values for "setup" and "start start: { 'order-2': 'started-as-2' }, }, ], - ] as Array<[PluginWrapper, Contracts]>); + ] as Array<[PluginWrapper, Contracts]>); const setupContextMap = new Map(); const startContextMap = new Map(); @@ -434,7 +433,7 @@ describe('setup', () => { afterAll(() => { jest.useRealTimers(); }); - it('throws timeout error if "setup" was not completed in 30 sec.', async () => { + it('throws timeout error if "setup" was not completed in 10 sec.', async () => { const plugin: PluginWrapper = createPlugin('timeout-setup'); jest.spyOn(plugin, 'setup').mockImplementation(() => new Promise((i) => i)); pluginsSystem.addPlugin(plugin); @@ -444,7 +443,7 @@ describe('setup', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); @@ -471,8 +470,8 @@ describe('start', () => { afterAll(() => { jest.useRealTimers(); }); - it('throws timeout error if "start" was not completed in 30 sec.', async () => { - const plugin: PluginWrapper = createPlugin('timeout-start'); + it('throws timeout error if "start" was not completed in 10 sec.', async () => { + const plugin = createPlugin('timeout-start'); jest.spyOn(plugin, 'setup').mockResolvedValue({}); jest.spyOn(plugin, 'start').mockImplementation(() => new Promise((i) => i)); @@ -485,7 +484,7 @@ describe('start', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); @@ -505,3 +504,120 @@ describe('start', () => { expect(log.info).toHaveBeenCalledWith(`Starting [2] plugins: [order-1,order-0]`); }); }); + +describe('asynchronous plugins', () => { + const runScenario = async ({ + production, + asyncSetup, + asyncStart, + }: { + production: boolean; + asyncSetup: boolean; + asyncStart: boolean; + }) => { + env = Env.createDefault( + REPO_ROOT, + getEnvOptions({ + cliArgs: { + dev: !production, + envName: production ? 'production' : 'development', + }, + }) + ); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; + pluginsSystem = new PluginsSystem(coreContext); + + const syncPlugin = createPlugin('sync-plugin'); + jest.spyOn(syncPlugin, 'setup').mockReturnValue('setup-sync'); + jest.spyOn(syncPlugin, 'start').mockReturnValue('start-sync'); + pluginsSystem.addPlugin(syncPlugin); + + const asyncPlugin = createPlugin('async-plugin'); + jest + .spyOn(asyncPlugin, 'setup') + .mockReturnValue(asyncSetup ? Promise.resolve('setup-async') : 'setup-sync'); + jest + .spyOn(asyncPlugin, 'start') + .mockReturnValue(asyncStart ? Promise.resolve('start-async') : 'start-sync'); + pluginsSystem.addPlugin(asyncPlugin); + + await pluginsSystem.setupPlugins(setupDeps); + await pluginsSystem.startPlugins(startDeps); + }; + + it('logs a warning if a plugin returns a promise from its setup contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: false, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its setup contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: true, + asyncStart: false, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs a warning if a plugin returns a promise from its start contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: false, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its start contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: false, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs multiple warnings if both `setup` and `start` return promises', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); +}); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 1b5e3bbb06e71..b7b8c297ea571 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from '@kbn/std'; +import { withTimeout, isPromise } from '@kbn/std'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; @@ -94,14 +94,25 @@ export class PluginsSystem { return depContracts; }, {} as Record); - const contract = await withTimeout({ - promise: plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.setup( + createPluginSetupContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + this.log.warn( + `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); @@ -132,14 +143,25 @@ export class PluginsSystem { return depContracts; }, {} as Record); - const contract = await withTimeout({ - promise: plugin.start( - createPluginStartContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + this.log.warn( + `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } contracts.set(pluginName, contract); } diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 91ccc2dedf272..45db98201b758 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -242,6 +242,23 @@ export interface Plugin< TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; +} + +/** + * A plugin with asynchronous lifecycle methods. + * + * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} + * @public + */ +export interface AsyncPlugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; @@ -383,4 +400,8 @@ export type PluginInitializer< TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; +> = ( + core: PluginInitializerContext +) => + | Plugin + | AsyncPlugin; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f3191c5625f8d..09207608908a4 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -203,6 +203,16 @@ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; } +// @public @deprecated +export interface AsyncPlugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + // @public (undocumented) export interface Authenticated extends AuthResultParams { // (undocumented) @@ -1815,9 +1825,9 @@ export { PackageInfo } // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart; // (undocumented) stop?(): void; } @@ -1836,7 +1846,7 @@ export interface PluginConfigDescriptor { export type PluginConfigSchema = Type; // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; // @public export interface PluginInitializerContext { @@ -3141,9 +3151,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:306:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:371:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:283:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:388:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/plugins/apm_oss/server/plugin.ts b/src/plugins/apm_oss/server/plugin.ts index fc3d105da5024..e504d5f0b9a9f 100644 --- a/src/plugins/apm_oss/server/plugin.ts +++ b/src/plugins/apm_oss/server/plugin.ts @@ -8,7 +8,6 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { APMOSSConfig } from './'; import { HomeServerPluginSetup, TutorialProvider } from '../../home/server'; import { tutorialProvider } from './tutorial'; @@ -17,10 +16,10 @@ export class APMOSSPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } - public async setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { + public setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { const config$ = this.initContext.config.create(); - const config = await config$.pipe(take(1)).toPromise(); + const config = this.initContext.config.get(); const apmTutorialProvider = tutorialProvider({ indexPatternTitle: config.indexPattern, @@ -35,6 +34,7 @@ export class APMOSSPlugin implements Plugin { plugins.home.tutorials.registerTutorial(apmTutorialProvider); return { + config, config$, getRegisteredTutorialProvider: () => apmTutorialProvider, }; @@ -45,6 +45,7 @@ export class APMOSSPlugin implements Plugin { } export interface APMOSSPluginSetup { + config: APMOSSConfig; config$: Observable; getRegisteredTutorialProvider(): TutorialProvider; } diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index b2f43b315aa9b..a5f1ca6107600 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { ProxyConfigCollection } from './lib'; @@ -28,7 +27,7 @@ export class ConsoleServerPlugin implements Plugin { this.log = this.ctx.logger.get(); } - async setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { + setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { capabilities.registerProvider(() => ({ dev_tools: { show: true, @@ -36,8 +35,8 @@ export class ConsoleServerPlugin implements Plugin { }, })); - const config = await this.ctx.config.create().pipe(first()).toPromise(); - const globalConfig = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise(); + const config = this.ctx.config.get(); + const globalConfig = this.ctx.config.legacy.get(); const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str)); this.esLegacyConfigService.setup(elasticsearch.legacy.config$); diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx index 6aee8b75757c2..93ffaa93cd80e 100644 --- a/src/plugins/inspector/public/plugin.tsx +++ b/src/plugins/inspector/public/plugin.tsx @@ -56,7 +56,7 @@ export class InspectorPublicPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} - public async setup(core: CoreSetup) { + public setup(core: CoreSetup) { this.views = new InspectorViewRegistry(); this.views.register(getRequestsViewDescription()); diff --git a/src/plugins/legacy_export/server/plugin.ts b/src/plugins/legacy_export/server/plugin.ts index 3433d076ee800..ac38f300bd02b 100644 --- a/src/plugins/legacy_export/server/plugin.ts +++ b/src/plugins/legacy_export/server/plugin.ts @@ -7,16 +7,13 @@ */ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; -import { first } from 'rxjs/operators'; import { registerRoutes } from './routes'; export class LegacyExportPlugin implements Plugin<{}, {}> { constructor(private readonly initContext: PluginInitializerContext) {} - public async setup({ http }: CoreSetup) { - const globalConfig = await this.initContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + public setup({ http }: CoreSetup) { + const globalConfig = this.initContext.config.legacy.get(); const router = http.createRouter(); registerRoutes( diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index 00d51da501834..4f35c1c1e5fc1 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -8,7 +8,6 @@ import { Plugin, PluginConfigDescriptor } from 'kibana/server'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { Observable } from 'rxjs'; import { configSchema, MapsLegacyConfig } from '../config'; import { getUiSettings } from './ui_settings'; @@ -30,7 +29,7 @@ export const config: PluginConfigDescriptor = { }; export interface MapsLegacyPluginSetup { - config$: Observable; + config: MapsLegacyConfig; } export class MapsLegacyPlugin implements Plugin { @@ -43,10 +42,9 @@ export class MapsLegacyPlugin implements Plugin { public setup(core: CoreSetup) { core.uiSettings.register(getUiSettings()); - // @ts-ignore - const config$ = this._initializerContext.config.create(); + const pluginConfig = this._initializerContext.config.get(); return { - config$, + config: pluginConfig, }; } diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index 15efbf38e7b93..6f74198bb56ab 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -31,10 +31,10 @@ export class PresentationUtilPlugin return {}; } - public async start( + public start( coreStart: CoreStart, startPlugins: PresentationUtilPluginStartDeps - ): Promise { + ): PresentationUtilPluginStart { pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); return { diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index d5d57da400a51..a3a2331cf8f76 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -79,7 +79,7 @@ export class RegionMapPlugin implements Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup) { - const config = await this.initializerContext.config - .create() - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup) { + const config = this.initializerContext.config.get(); const collectorSet = new CollectorSet({ logger: this.logger.get('collector-set'), maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, }); - const globalConfig = await this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initializerContext.config.legacy.get(); const router = core.http.createRouter(); setupRoutes({ diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 4792ceefde536..0a9d477c26691 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, AsyncPlugin } from 'kibana/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; @@ -34,8 +34,7 @@ export interface TablePluginStartDependencies { /** @internal */ export class TableVisPlugin - implements - Plugin, void, TablePluginSetupDependencies, TablePluginStartDependencies> { + implements AsyncPlugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/vis_type_timelion/server/index.ts b/src/plugins/vis_type_timelion/server/index.ts index e31aae7fcdda7..1dcb7263c4818 100644 --- a/src/plugins/vis_type_timelion/server/index.ts +++ b/src/plugins/vis_type_timelion/server/index.ts @@ -8,7 +8,7 @@ import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { configSchema, ConfigSchema } from '../config'; -import { Plugin } from './plugin'; +import { TimelionPlugin } from './plugin'; export { PluginSetupContract } from './plugin'; @@ -25,4 +25,4 @@ export const config: PluginConfigDescriptor = { ], }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new TimelionPlugin(initializerContext); diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index 2bb8f7214f904..c1800a09ba35c 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -7,13 +7,12 @@ */ import { i18n } from '@kbn/i18n'; -import { first } from 'rxjs/operators'; import { TypeOf, schema } from '@kbn/config-schema'; import { RecursiveReadonly } from '@kbn/utility-types'; import { deepFreeze } from '@kbn/std'; import type { PluginStart, DataRequestHandlerContext } from '../../../../src/plugins/data/server'; -import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server'; +import { CoreSetup, PluginInitializerContext, Plugin } from '../../../../src/core/server'; import { configSchema } from '../config'; import loadFunctions from './lib/load_functions'; import { functionsRoute } from './routes/functions'; @@ -39,16 +38,12 @@ export interface TimelionPluginStartDeps { /** * Represents Timelion Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class TimelionPlugin + implements Plugin, void, TimelionPluginStartDeps> { constructor(private readonly initializerContext: PluginInitializerContext) {} - public async setup( - core: CoreSetup - ): Promise> { - const config = await this.initializerContext.config - .create>() - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup): RecursiveReadonly { + const config = this.initializerContext.config.get>(); const configManager = new ConfigManager(this.initializerContext.config); diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index 59ae89300705e..6900630ffa971 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -43,14 +43,14 @@ export interface MetricsPluginStartDependencies { } /** @internal */ -export class MetricsPlugin implements Plugin, void> { +export class MetricsPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( + public setup( core: CoreSetup, { expressions, visualizations, charts, visualize }: MetricsPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index a01af7484ea99..7cc70f31589c7 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -54,14 +54,14 @@ export interface VegaPluginStartDependencies { } /** @internal */ -export class VegaPlugin implements Plugin, void> { +export class VegaPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( + public setup( core: CoreSetup, { inspector, data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index b266a681f8031..9d329c92bede0 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -46,7 +46,7 @@ export class VisTypeVislibPlugin Plugin { constructor(public initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: VisTypeVislibCoreSetup, { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index 5be971a085d3c..75a2f4fb6895c 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -59,7 +59,7 @@ export class VisTypeXyPlugin VisTypeXyPluginSetupDependencies, VisTypeXyPluginStartDependencies > { - public async setup( + public setup( core: VisTypeXyCoreSetup, { expressions, visualizations, charts, usageCollection }: VisTypeXyPluginSetupDependencies ) { diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 1cad0ca7ca396..3d82e6c60a1b6 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -84,7 +84,7 @@ export class VisualizePlugin constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, { home, urlForwarding, data }: VisualizePluginSetupDependencies ) { diff --git a/test/plugin_functional/plugins/app_link_test/public/plugin.ts b/test/plugin_functional/plugins/app_link_test/public/plugin.ts index 7f92cdccd7243..8d75cb09469bc 100644 --- a/test/plugin_functional/plugins/app_link_test/public/plugin.ts +++ b/test/plugin_functional/plugins/app_link_test/public/plugin.ts @@ -10,7 +10,7 @@ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { renderApp } from './app'; export class CoreAppLinkPlugin implements Plugin { - public async setup(core: CoreSetup, deps: {}) { + public setup(core: CoreSetup, deps: {}) { core.application.register({ id: 'applink_start', title: 'AppLink Start', diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index 6a167b17befd1..48c8d85b21dac 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -42,7 +42,7 @@ export class CorePluginBPlugin }; } - public async start(core: CoreStart, deps: {}) { + public start(core: CoreStart, deps: {}) { return { sendSystemRequest: async (asSystemRequest: boolean) => { const response = await core.http.post('/core_plugin_b/system_request', { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 9797a55fa0e3d..8fbacc71d30cb 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -6,9 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { first } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { Observable } from 'rxjs'; import { PluginInitializerContext, Plugin, @@ -136,11 +134,9 @@ const includedHiddenTypes = [ ALERT_SAVED_OBJECT_TYPE, ]; -export class ActionsPlugin implements Plugin, PluginStartContract> { - private readonly config: Promise; - +export class ActionsPlugin implements Plugin { private readonly logger: Logger; - private actionsConfig?: ActionsConfig; + private readonly actionsConfig: ActionsConfig; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; private actionExecutor?: ActionExecutor; @@ -151,20 +147,20 @@ export class ActionsPlugin implements Plugin, Plugi private isESOUsingEphemeralEncryptionKey?: boolean; private readonly telemetryLogger: Logger; private readonly preconfiguredActions: PreConfiguredAction[]; - private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; + private readonly kibanaIndexConfig: { kibana: { index: string } }; constructor(initContext: PluginInitializerContext) { - this.config = initContext.config.create().pipe(first()).toPromise(); + this.actionsConfig = initContext.config.get(); this.logger = initContext.logger.get('actions'); this.telemetryLogger = initContext.logger.get('usage'); this.preconfiguredActions = []; - this.kibanaIndexConfig = initContext.config.legacy.globalConfig$; + this.kibanaIndexConfig = initContext.config.legacy.get(); } - public async setup( + public setup( core: CoreSetup, plugins: ActionsPluginsSetup - ): Promise { + ): PluginSetupContract { this.licenseState = new LicenseState(plugins.licensing.license$); this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; @@ -190,7 +186,6 @@ export class ActionsPlugin implements Plugin, Plugi // get executions count const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); - this.actionsConfig = (await this.config) as ActionsConfig; const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig); for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) { @@ -229,20 +224,18 @@ export class ActionsPlugin implements Plugin, Plugi ); } - this.kibanaIndexConfig.subscribe((config) => { - core.http.registerRouteHandlerContext( - 'actions', - this.createRouteHandlerContext(core, config.kibana.index) + core.http.registerRouteHandlerContext( + 'actions', + this.createRouteHandlerContext(core, this.kibanaIndexConfig.kibana.index) + ); + if (usageCollection) { + initializeActionsTelemetry( + this.telemetryLogger, + plugins.taskManager, + core, + this.kibanaIndexConfig.kibana.index ); - if (usageCollection) { - initializeActionsTelemetry( - this.telemetryLogger, - plugins.taskManager, - core, - config.kibana.index - ); - } - }); + } // Routes const router = core.http.createRouter(); @@ -304,7 +297,7 @@ export class ActionsPlugin implements Plugin, Plugi request ); - const kibanaIndex = (await kibanaIndexConfig.pipe(first()).toPromise()).kibana.index; + const kibanaIndex = kibanaIndexConfig.kibana.index; return new ActionsClient({ unsecuredSavedObjectsClient, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e2840dbdf5ef7..49fded8649c46 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -61,7 +61,7 @@ export class APMPlugin implements Plugin { this.initContext = initContext; } - public async setup( + public setup( core: CoreSetup, plugins: { apmOss: APMOSSPluginSetup; @@ -98,7 +98,10 @@ export class APMPlugin implements Plugin { }); } - this.currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); + this.currentConfig = mergeConfigs( + plugins.apmOss.config, + this.initContext.config.get() + ); if ( plugins.taskManager && diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts index 6a814f68a67f4..3093d5d9b8d29 100644 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { take } from 'rxjs/operators'; -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext, Logger } from 'src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginStart } from '../../licensing/server'; @@ -27,14 +26,17 @@ interface StartDeps { } export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDeps> { + private readonly logger: Logger; private securitySetup?: SecurityPluginSetup; private beatsLibs?: CMServerLibs; constructor( private readonly initializerContext: PluginInitializerContext - ) {} + ) { + this.logger = initializerContext.logger.get(); + } - public async setup(core: CoreSetup, { features, security }: SetupDeps) { + public setup(core: CoreSetup, { features, security }: SetupDeps) { this.securitySetup = security; const router = core.http.createRouter(); @@ -64,8 +66,8 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep return {}; } - public async start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { - const config = await this.initializerContext.config.create().pipe(take(1)).toPromise(); + public start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { + const config = this.initializerContext.config.get(); const logger = this.initializerContext.logger.get(); const kibanaVersion = this.initializerContext.env.packageInfo.version; @@ -78,7 +80,9 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep kibanaVersion, }); - await this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate); + this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate).catch((e) => { + this.logger.error(`Error create beats template: ${e.message}`); + }); return {}; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 7387ae1a203c2..345f6099009fc 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { CoreSetup, PluginInitializerContext, Plugin, Logger, CoreStart } from 'src/core/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; @@ -34,7 +33,7 @@ export class CanvasPlugin implements Plugin { this.logger = initializerContext.logger.get(); } - public async setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { coreSetup.savedObjects.registerType(customElementType); coreSetup.savedObjects.registerType(workpadType); coreSetup.savedObjects.registerType(workpadTemplateType); @@ -84,9 +83,7 @@ export class CanvasPlugin implements Plugin { ); // we need the kibana index provided by global config for the Canvas usage collector - const globalConfig = await this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initializerContext.config.legacy.get(); registerCanvasUsageCollector(plugins.usageCollection, globalConfig.kibana.index); setupInterpreter(plugins.expressions); diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 589093461a5e0..8b4fdc73dab44 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first, map } from 'rxjs/operators'; import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; @@ -38,8 +37,8 @@ import { createCaseClient } from './client'; import { registerConnectors } from './connectors'; import type { CasesRequestHandlerContext } from './types'; -function createConfig$(context: PluginInitializerContext) { - return context.config.create().pipe(map((config) => config)); +function createConfig(context: PluginInitializerContext) { + return context.config.get(); } export interface PluginsSetup { @@ -60,7 +59,7 @@ export class CasePlugin { } public async setup(core: CoreSetup, plugins: PluginsSetup) { - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + const config = createConfig(this.initializerContext); if (!config.enabled) { return; @@ -118,7 +117,7 @@ export class CasePlugin { }); } - public async start(core: CoreStart) { + public start(core: CoreStart) { this.log.debug(`Starting Case Workflow`); this.alertsService!.initialize(core.elasticsearch.client); diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index eeb295b264f60..4c12aa3d92b47 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -45,7 +45,7 @@ export class CloudPlugin implements Plugin { this.isCloudEnabled = false; } - public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { + public setup(core: CoreSetup, { home }: CloudSetupDependencies) { const { id, resetPasswordUrl, deploymentUrl } = this.config; this.isCloudEnabled = getIsCloudEnabled(id); diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index 55ed72ca01957..6abfb864d1cd0 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; -import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; import { CloudConfigType } from './config'; @@ -28,25 +26,24 @@ export interface CloudSetup { export class CloudPlugin implements Plugin { private readonly logger: Logger; - private readonly config$: Observable; + private readonly config: CloudConfigType; constructor(private readonly context: PluginInitializerContext) { this.logger = this.context.logger.get(); - this.config$ = this.context.config.create(); + this.config = this.context.config.get(); } - public async setup(core: CoreSetup, { usageCollection }: PluginsSetup) { + public setup(core: CoreSetup, { usageCollection }: PluginsSetup) { this.logger.debug('Setting up Cloud plugin'); - const config = await this.config$.pipe(first()).toPromise(); - const isCloudEnabled = getIsCloudEnabled(config.id); + const isCloudEnabled = getIsCloudEnabled(this.config.id); registerCloudUsageCollector(usageCollection, { isCloudEnabled }); return { - cloudId: config.id, + cloudId: this.config.id, isCloudEnabled, apm: { - url: config.apm?.url, - secretToken: config.apm?.secret_token, + url: this.config.apm?.url, + secretToken: this.config.apm?.secret_token, }, }; } diff --git a/x-pack/plugins/code/server/plugin.ts b/x-pack/plugins/code/server/plugin.ts index c9197a30b5214..eb7481d12387d 100644 --- a/x-pack/plugins/code/server/plugin.ts +++ b/x-pack/plugins/code/server/plugin.ts @@ -5,22 +5,18 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, Plugin } from 'src/core/server'; import { CodeConfigSchema } from './config'; /** * Represents Code Plugin instance that will be managed by the Kibana plugin system. */ -export class CodePlugin { +export class CodePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public async setup() { - const config = await this.initializerContext.config - .create>() - .pipe(first()) - .toPromise(); + const config = this.initializerContext.config.get>(); if (config && Object.keys(config).length > 0) { this.initializerContext.logger diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 8097c22cfbabc..53b020e5b8241 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './config'; -import { Plugin } from './plugin'; +import { EncryptedSavedObjectsPlugin } from './plugin'; export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto'; export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; @@ -15,4 +15,4 @@ export { EncryptedSavedObjectsClient } from './saved_objects'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new EncryptedSavedObjectsPlugin(initializerContext); diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 2324c31b13d00..823a6b0afa9dc 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Plugin } from './plugin'; +import { EncryptedSavedObjectsPlugin } from './plugin'; import { ConfigSchema } from './config'; import { coreMock } from 'src/core/server/mocks'; @@ -13,12 +13,12 @@ import { securityMock } from '../../security/server/mocks'; describe('EncryptedSavedObjects Plugin', () => { describe('setup()', () => { - it('exposes proper contract', async () => { - const plugin = new Plugin( + it('exposes proper contract', () => { + const plugin = new EncryptedSavedObjectsPlugin( coreMock.createPluginInitializerContext(ConfigSchema.validate({}, { dist: true })) ); - await expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) - .resolves.toMatchInlineSnapshot(` + expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) + .toMatchInlineSnapshot(` Object { "createMigration": [Function], "registerType": [Function], @@ -29,14 +29,14 @@ describe('EncryptedSavedObjects Plugin', () => { }); describe('start()', () => { - it('exposes proper contract', async () => { - const plugin = new Plugin( + it('exposes proper contract', () => { + const plugin = new EncryptedSavedObjectsPlugin( coreMock.createPluginInitializerContext(ConfigSchema.validate({}, { dist: true })) ); - await plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }); + plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }); const startContract = plugin.start(); - await expect(startContract).toMatchInlineSnapshot(` + expect(startContract).toMatchInlineSnapshot(` Object { "getClient": [Function], "isEncryptionError": [Function], diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index bfc757accaa82..e846b133c26e0 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { first, map } from 'rxjs/operators'; import nodeCrypto from '@elastic/node-crypto'; -import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; +import { Logger, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import { SecurityPluginSetup } from '../../security/server'; import { createConfig, ConfigSchema } from './config'; @@ -40,7 +39,9 @@ export interface EncryptedSavedObjectsPluginStart { /** * Represents EncryptedSavedObjects Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class EncryptedSavedObjectsPlugin + implements + Plugin { private readonly logger: Logger; private savedObjectsSetup!: ClientInstanciator; @@ -48,17 +49,11 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup( - core: CoreSetup, - deps: PluginsSetup - ): Promise { - const config = await this.initializerContext.config - .create>() - .pipe( - map((rawConfig) => createConfig(rawConfig, this.initializerContext.logger.get('config'))) - ) - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup, deps: PluginsSetup): EncryptedSavedObjectsPluginSetup { + const config = createConfig( + this.initializerContext.config.get>(), + this.initializerContext.logger.get('config') + ); const auditLogger = new EncryptedSavedObjectsAuditLogger( deps.security?.audit.getLogger('encryptedSavedObjects') ); diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 4ea8ef2c089e4..569479f921cdd 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { Plugin, PluginInitializerContext, @@ -66,19 +64,19 @@ export interface RouteDependencies { } export class EnterpriseSearchPlugin implements Plugin { - private config: Observable; - private logger: Logger; + private readonly config: ConfigType; + private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { - this.config = initializerContext.config.create(); + this.config = initializerContext.config.get(); this.logger = initializerContext.logger.get(); } - public async setup( + public setup( { capabilities, http, savedObjects, getStartServices }: CoreSetup, { usageCollection, security, features }: PluginsSetup ) { - const config = await this.config.pipe(first()).toPromise(); + const config = this.config; const log = this.logger; /** diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 04be4ce67c12d..9cc874735cc0e 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, @@ -24,7 +22,6 @@ import type { IEventLogConfig, IEventLogService, IEventLogger, - IEventLogConfig$, IEventLogClientService, } from './types'; import { findRoute } from './routes'; @@ -48,32 +45,29 @@ interface PluginStartDeps { } export class Plugin implements CorePlugin { - private readonly config$: IEventLogConfig$; + private readonly config: IEventLogConfig; private systemLogger: Logger; private eventLogService?: EventLogService; private esContext?: EsContext; private eventLogger?: IEventLogger; - private globalConfig$: Observable; + private globalConfig: SharedGlobalConfig; private eventLogClientService?: EventLogClientService; private savedObjectProviderRegistry: SavedObjectProviderRegistry; private kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; constructor(private readonly context: PluginInitializerContext) { this.systemLogger = this.context.logger.get(); - this.config$ = this.context.config.create(); - this.globalConfig$ = this.context.config.legacy.globalConfig$; + this.config = this.context.config.get(); + this.globalConfig = this.context.config.legacy.get(); this.savedObjectProviderRegistry = new SavedObjectProviderRegistry(); this.kibanaVersion = this.context.env.packageInfo.version; } - async setup(core: CoreSetup): Promise { - const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); - const kibanaIndex = globalConfig.kibana.index; + setup(core: CoreSetup): IEventLogService { + const kibanaIndex = this.globalConfig.kibana.index; this.systemLogger.debug('setting up plugin'); - const config = await this.config$.pipe(first()).toPromise(); - this.esContext = createEsContext({ logger: this.systemLogger, // TODO: get index prefix from config.get(kibana.index) @@ -85,7 +79,7 @@ export class Plugin implements CorePlugin { + start(core: CoreStart, { spaces }: PluginStartDeps): IEventLogClientService { this.systemLogger.debug('starting plugin'); if (!this.esContext) throw new Error('esContext not initialized'); diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 786f5ba587d26..0e5e62b591290 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import type { IRouter, KibanaRequest, RequestHandlerContext } from 'src/core/server'; @@ -25,7 +24,6 @@ export const ConfigSchema = schema.object({ }); export type IEventLogConfig = TypeOf; -export type IEventLogConfig$ = Observable>; // the object exposed by plugin.setup() export interface IEventLogService { diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 111f294b6ad55..0890274fed950 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -6,7 +6,7 @@ */ import { PluginInitializerContext } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { FeaturesPlugin } from './plugin'; // These exports are part of public Features plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. Ideally we should @@ -25,4 +25,4 @@ export { export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new FeaturesPlugin(initializerContext); diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 4462edeed9510..0de03e54e1f79 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -6,7 +6,7 @@ */ import { coreMock, savedObjectsServiceMock } from 'src/core/server/mocks'; -import { Plugin } from './plugin'; +import { FeaturesPlugin } from './plugin'; describe('Features Plugin', () => { let initContext: ReturnType; @@ -31,7 +31,7 @@ describe('Features Plugin', () => { }); it('returns OSS + registered kibana features', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerKibanaFeature } = await plugin.setup(coreSetup, {}); registerKibanaFeature({ id: 'baz', @@ -58,7 +58,7 @@ describe('Features Plugin', () => { }); it('returns OSS + registered kibana features with timelion when available', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, { visTypeTimelion: { uiEnabled: true }, }); @@ -88,7 +88,7 @@ describe('Features Plugin', () => { }); it('registers kibana features with not hidden saved objects types', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); const { getKibanaFeatures } = plugin.start(coreStart); @@ -101,7 +101,7 @@ describe('Features Plugin', () => { }); it('returns registered elasticsearch features', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerElasticsearchFeature } = await plugin.setup(coreSetup, {}); registerElasticsearchFeature({ id: 'baz', @@ -123,7 +123,7 @@ describe('Features Plugin', () => { }); it('registers a capabilities provider', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index e96c257516b98..6a9fd1da826a6 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -12,6 +12,7 @@ import { CoreStart, SavedObjectsServiceStart, Logger, + Plugin, PluginInitializerContext, } from '../../../../src/core/server'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; @@ -59,7 +60,9 @@ interface TimelionSetupContract { /** * Represents Features Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class FeaturesPlugin + implements + Plugin, RecursiveReadonly> { private readonly logger: Logger; private readonly featureRegistry: FeatureRegistry = new FeatureRegistry(); private isTimelionEnabled: boolean = false; @@ -68,10 +71,10 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup( + public setup( core: CoreSetup, { visTypeTimelion }: { visTypeTimelion?: TimelionSetupContract } - ): Promise> { + ): RecursiveReadonly { this.isTimelionEnabled = visTypeTimelion !== undefined && visTypeTimelion.uiEnabled; defineRoutes({ diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index fce8e89a6573d..50e647e271ecc 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -155,7 +155,7 @@ export class FleetPlugin implements Plugin { + public start(core: CoreStart): FleetStart { let successPromise: ReturnType; return { diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 1aa6b42611a34..7378d45e1bb3a 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -12,7 +12,7 @@ import { CoreStart, ElasticsearchServiceStart, Logger, - Plugin, + AsyncPlugin, PluginInitializerContext, SavedObjectsServiceStart, HttpServiceSetup, @@ -169,7 +169,7 @@ export interface FleetStartContract { } export class FleetPlugin - implements Plugin { + implements AsyncPlugin { private licensing$!: Observable; private config$: Observable; private cloud: CloudSetup | undefined; diff --git a/x-pack/plugins/global_search/server/plugin.ts b/x-pack/plugins/global_search/server/plugin.ts index 8d560d9a0f553..d7c06a92f70e0 100644 --- a/x-pack/plugins/global_search/server/plugin.ts +++ b/x-pack/plugins/global_search/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; import { LicensingPluginStart } from '../../licensing/server'; import { LicenseChecker, ILicenseChecker } from '../common/license_checker'; @@ -33,20 +31,19 @@ export class GlobalSearchPlugin GlobalSearchPluginSetupDeps, GlobalSearchPluginStartDeps > { - private readonly config$: Observable; + private readonly config: GlobalSearchConfigType; private readonly searchService = new SearchService(); private searchServiceStart?: SearchServiceStart; private licenseChecker?: ILicenseChecker; constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); + this.config = context.config.get(); } - public async setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { - const config = await this.config$.pipe(take(1)).toPromise(); + public setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { const { registerResultProvider } = this.searchService.setup({ basePath: core.http.basePath, - config, + config: this.config, }); registerRoutes(core.http.createRouter()); diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index 5c13756842039..32dac5fba86f9 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -20,7 +20,7 @@ import { graphWorkspace } from './saved_objects'; export class GraphPlugin implements Plugin { private licenseState: LicenseState | null = null; - public async setup( + public setup( core: CoreSetup, { licensing, diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 532cf253c7f89..95793c0cad465 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { CoreSetup, @@ -52,22 +50,19 @@ const indexLifecycleDataEnricher = async ( }; export class IndexLifecycleManagementServerPlugin implements Plugin { - private readonly config$: Observable; + private readonly config: IndexLifecycleManagementConfig; private readonly license: License; private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); - this.config$ = initializerContext.config.create(); + this.config = initializerContext.config.get(); this.license = new License(); } - async setup( - { http }: CoreSetup, - { licensing, indexManagement, features }: Dependencies - ): Promise { + setup({ http }: CoreSetup, { licensing, indexManagement, features }: Dependencies): void { const router = http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); + const config = this.config; this.license.setup( { diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index e91e085207cb7..99555fa56acd5 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -8,8 +8,7 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { Observable } from 'rxjs'; -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; @@ -79,22 +78,15 @@ export interface InfraPluginSetup { ) => void; } -export class InfraServerPlugin { - private config$: Observable; - public config = {} as InfraConfig; +export class InfraServerPlugin implements Plugin { + public config: InfraConfig; public libs: InfraBackendLibs | undefined; constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); + this.config = context.config.get(); } - async setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { - await new Promise((resolve) => { - this.config$.subscribe((configValue) => { - this.config = configValue; - resolve(); - }); - }); + setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { const framework = new KibanaFramework(core, this.config, plugins); const sources = new InfraSources({ config: this.config, diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 0207f79310273..1db463a47dbf0 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -123,7 +123,7 @@ export class LicensingPlugin implements Plugin { private stop$ = new Subject(); private readonly logger: Logger; - private readonly config$: Observable; + private readonly config: LicenseConfigType; private loggingSubscription?: Subscription; private featureUsage = new FeatureUsageService(); @@ -92,13 +91,12 @@ export class LicensingPlugin implements Plugin(); + this.config = this.context.config.get(); } - public async setup(core: CoreSetup<{}, LicensingPluginStart>) { + public setup(core: CoreSetup<{}, LicensingPluginStart>) { this.logger.debug('Setting up Licensing plugin'); - const config = await this.config$.pipe(take(1)).toPromise(); - const pollingFrequency = config.api_polling_frequency; + const pollingFrequency = this.config.api_polling_frequency; async function callAsInternalUser( ...args: Parameters @@ -225,7 +223,7 @@ export class LicensingPlugin implements Plugin> => { - return context.config.create().pipe(map((config) => config)); -}; diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index bc064e236b658..b79d6a0b89a57 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import type { CoreSetup, CoreStart } from 'src/core/server'; @@ -23,7 +22,6 @@ import type { ListsRequestHandlerContext, PluginsStart, } from './types'; -import { createConfig$ } from './create_config'; import { getSpaceId } from './get_space_id'; import { getUser } from './get_user'; import { initSavedObjects } from './saved_objects'; @@ -32,17 +30,17 @@ import { ExceptionListClient } from './services/exception_lists/exception_list_c export class ListPlugin implements Plugin, ListsPluginStart, {}, PluginsStart> { private readonly logger: Logger; + private readonly config: ConfigType; private spaces: SpacesServiceStart | undefined | null; - private config: ConfigType | undefined | null; private security: SecurityPluginStart | undefined | null; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); + this.config = this.initializerContext.config.get(); } public async setup(core: CoreSetup): Promise { - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); - this.config = config; + const { config } = this; initSavedObjects(core.savedObjects); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 786e35212ec7b..7440b6ee1e1df 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { take } from 'rxjs/operators'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; // @ts-ignore @@ -134,12 +133,11 @@ export class MapsPlugin implements Plugin { } // @ts-ignore - async setup(core: CoreSetup, plugins: SetupDeps) { + setup(core: CoreSetup, plugins: SetupDeps) { const { usageCollection, home, licensing, features, mapsLegacy } = plugins; - // @ts-ignore + const mapsLegacyConfig = mapsLegacy.config; const config$ = this._initializerContext.config.create(); - const mapsLegacyConfig = await mapsLegacy.config$.pipe(take(1)).toPromise(); - const currentConfig = await config$.pipe(take(1)).toPromise(); + const currentConfig = this._initializerContext.config.get(); // @ts-ignore const mapsEnabled = currentConfig.enabled; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 3d3671ac0a6a4..b950b064774b1 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -49,7 +49,7 @@ export class MonitoringPlugin Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { diff --git a/x-pack/plugins/monitoring/server/index.ts b/x-pack/plugins/monitoring/server/index.ts index 012c050cd3fa8..97e572d15327c 100644 --- a/x-pack/plugins/monitoring/server/index.ts +++ b/x-pack/plugins/monitoring/server/index.ts @@ -7,13 +7,15 @@ import { TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { MonitoringPlugin } from './plugin'; import { configSchema } from './config'; import { deprecations } from './deprecations'; export { KibanaSettingsCollector } from './kibana_monitoring/collectors'; export { MonitoringConfig } from './config'; -export const plugin = (initContext: PluginInitializerContext) => new Plugin(initContext); +export { MonitoringPluginSetup, IBulkUploader } from './types'; + +export const plugin = (initContext: PluginInitializerContext) => new MonitoringPlugin(initContext); export const config: PluginConfigDescriptor> = { schema: configSchema, deprecations, diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 2a5138d0d8880..08224980a558f 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.test.ts @@ -6,16 +6,9 @@ */ import { coreMock } from 'src/core/server/mocks'; -import { Plugin } from './plugin'; -import { combineLatest } from 'rxjs'; +import { MonitoringPlugin } from './plugin'; import { AlertsFactory } from './alerts'; -jest.mock('rxjs', () => ({ - // @ts-ignore - ...jest.requireActual('rxjs'), - combineLatest: jest.fn(), -})); - jest.mock('./es_client/instantiate_client', () => ({ instantiateClient: jest.fn().mockImplementation(() => ({ cluster: {}, @@ -32,30 +25,11 @@ jest.mock('./kibana_monitoring/collectors', () => ({ registerCollectors: jest.fn(), })); -describe('Monitoring plugin', () => { - const initializerContext = { - logger: { - get: jest.fn().mockImplementation(() => ({ - info: jest.fn(), - })), - }, - config: { - create: jest.fn().mockImplementation(() => ({ - pipe: jest.fn().mockImplementation(() => ({ - toPromise: jest.fn(), - })), - })), - legacy: { - globalConfig$: {}, - }, - }, - env: { - packageInfo: { - version: '1.0.0', - }, - }, - }; +jest.mock('./config', () => ({ + createConfig: (config: any) => config, +})); +describe('Monitoring plugin', () => { const coreSetup = coreMock.createSetup(); coreSetup.http.getServerInfo.mockReturnValue({ port: 5601 } as any); coreSetup.status.overall$.subscribe = jest.fn(); @@ -71,7 +45,6 @@ describe('Monitoring plugin', () => { }, }; - let config = {}; const defaultConfig = { ui: { elasticsearch: {}, @@ -83,20 +56,7 @@ describe('Monitoring plugin', () => { }, }; - beforeEach(() => { - config = defaultConfig; - (combineLatest as jest.Mock).mockImplementation(() => { - return { - pipe: jest.fn().mockImplementation(() => { - return { - toPromise: jest.fn().mockImplementation(() => { - return [config, 2]; - }), - }; - }), - }; - }); - }); + const initializerContext = coreMock.createPluginInitializerContext(defaultConfig); afterEach(() => { (setupPlugins.alerts.registerType as jest.Mock).mockReset(); @@ -104,14 +64,14 @@ describe('Monitoring plugin', () => { }); it('always create the bulk uploader', async () => { - const plugin = new Plugin(initializerContext as any); + const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup, setupPlugins as any); expect(coreSetup.status.overall$.subscribe).toHaveBeenCalled(); }); it('should register all alerts', async () => { const alerts = AlertsFactory.getAll(); - const plugin = new Plugin(initializerContext as any); + const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup as any, setupPlugins as any); expect(setupPlugins.alerts.registerType).toHaveBeenCalledTimes(alerts.length); }); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 6fd9e7534ac65..654c3de7d81a9 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -6,8 +6,6 @@ */ import Boom from '@hapi/boom'; -import { combineLatest } from 'rxjs'; -import { first, map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { has, get } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; @@ -21,6 +19,7 @@ import { CoreStart, CustomHttpResponseOptions, ResponseError, + Plugin, } from 'kibana/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { @@ -43,6 +42,7 @@ import { AlertsFactory } from './alerts'; import { MonitoringCore, MonitoringLicenseService, + MonitoringPluginSetup, LegacyShimDependencies, IBulkUploader, PluginsSetup, @@ -66,7 +66,8 @@ const wrapError = (error: any): CustomHttpResponseOptions => { }; }; -export class Plugin { +export class MonitoringPlugin + implements Plugin { private readonly initializerContext: PluginInitializerContext; private readonly log: Logger; private readonly getLogger: (...scopes: string[]) => Logger; @@ -82,15 +83,9 @@ export class Plugin { this.getLogger = (...scopes: string[]) => initializerContext.logger.get(LOGGING_TAG, ...scopes); } - async setup(core: CoreSetup, plugins: PluginsSetup) { - const [config, legacyConfig] = await combineLatest([ - this.initializerContext.config - .create>() - .pipe(map((rawConfig) => createConfig(rawConfig))), - this.initializerContext.config.legacy.globalConfig$, - ]) - .pipe(first()) - .toPromise(); + setup(core: CoreSetup, plugins: PluginsSetup) { + const config = createConfig(this.initializerContext.config.get>()); + const legacyConfig = this.initializerContext.config.legacy.get(); const router = core.http.createRouter(); this.legacyShimDependencies = { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 0fd30189c5415..bb0b616d37eac 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -94,6 +94,10 @@ export interface IBulkUploader { stop: () => void; } +export interface MonitoringPluginSetup { + getKibanaStats: IBulkUploader['getKibanaStats']; +} + export interface LegacyRequest { logger: Logger; getLogger: (...scopes: string[]) => Logger; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 62a330442fc29..a5843d1c4ade1 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -6,7 +6,6 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; -import { take } from 'rxjs/operators'; import { ObservabilityConfig } from '.'; import { bootstrapAnnotations, @@ -28,10 +27,8 @@ export class ObservabilityPlugin implements Plugin { this.initContext = initContext; } - public async setup(core: CoreSetup, plugins: {}): Promise { - const config$ = this.initContext.config.create(); - - const config = await config$.pipe(take(1)).toPromise(); + public setup(core: CoreSetup, plugins: {}): ObservabilityPluginSetup { + const config = this.initContext.config.get(); let annotationsApiPromise: Promise | undefined; diff --git a/x-pack/plugins/osquery/server/create_config.ts b/x-pack/plugins/osquery/server/create_config.ts index 19859ab05e6a9..d52f299a692cf 100644 --- a/x-pack/plugins/osquery/server/create_config.ts +++ b/x-pack/plugins/osquery/server/create_config.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { map } from 'rxjs/operators'; import { PluginInitializerContext } from 'kibana/server'; -import { Observable } from 'rxjs'; import { ConfigType } from './config'; -export const createConfig$ = ( - context: PluginInitializerContext -): Observable> => { - return context.config.create().pipe(map((config) => config)); +export const createConfig = (context: PluginInitializerContext): Readonly => { + return context.config.get(); }; diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 77509275431e9..c30f4ac057ec0 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { PluginInitializerContext, CoreSetup, @@ -14,7 +13,7 @@ import { Logger, } from '../../../../src/core/server'; -import { createConfig$ } from './create_config'; +import { createConfig } from './create_config'; import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; import { defineRoutes } from './routes'; import { osquerySearchStrategyProvider } from './search_strategy/osquery'; @@ -26,9 +25,9 @@ export class OsqueryPlugin implements Plugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('osquery: Setup'); - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + const config = createConfig(this.initializerContext); if (!config.enabled) { return {}; diff --git a/x-pack/plugins/painless_lab/server/plugin.ts b/x-pack/plugins/painless_lab/server/plugin.ts index aefb5429a1b13..996adfdc13f64 100644 --- a/x-pack/plugins/painless_lab/server/plugin.ts +++ b/x-pack/plugins/painless_lab/server/plugin.ts @@ -22,7 +22,7 @@ export class PainlessLabServerPlugin implements Plugin { this.license = new License(); } - async setup({ http }: CoreSetup, { licensing }: Dependencies) { + setup({ http }: CoreSetup, { licensing }: Dependencies) { const router = http.createRouter(); this.license.setup( diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index ba78cf411e369..2b8d9afe979e8 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -8,8 +8,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; @@ -29,17 +27,16 @@ export class RemoteClustersServerPlugin implements Plugin { licenseStatus: LicenseStatus; log: Logger; - config$: Observable; + config: ConfigType; constructor({ logger, config }: PluginInitializerContext) { this.log = logger.get(); - this.config$ = config.create(); + this.config = config.get(); this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { + setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { const router = http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); const routeDependencies: RouteDependencies = { router, @@ -89,7 +86,7 @@ export class RemoteClustersServerPlugin }); return { - isUiEnabled: config.ui.enabled, + isUiEnabled: this.config.ui.enabled, }; } diff --git a/x-pack/plugins/searchprofiler/server/plugin.ts b/x-pack/plugins/searchprofiler/server/plugin.ts index cebcbb1a0dc92..ed85febac9a45 100644 --- a/x-pack/plugins/searchprofiler/server/plugin.ts +++ b/x-pack/plugins/searchprofiler/server/plugin.ts @@ -21,7 +21,7 @@ export class SearchProfilerServerPlugin implements Plugin { this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { + setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { const router = http.createRouter(); profileRoute.register({ router, diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 6026e42676c57..66b916ac7f70f 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -15,7 +15,7 @@ import type { import { ConfigSchema } from './config'; import { securityConfigDeprecationProvider } from './config_deprecations'; import { - Plugin, + SecurityPlugin, SecurityPluginSetup, SecurityPluginStart, PluginSetupDependencies, @@ -51,4 +51,4 @@ export const plugin: PluginInitializer< RecursiveReadonly, RecursiveReadonly, PluginSetupDependencies -> = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); +> = (initializerContext: PluginInitializerContext) => new SecurityPlugin(initializerContext); diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 84fc410c72cd0..d57951ecb5b1d 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -8,7 +8,7 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; import { ConfigSchema } from './config'; -import { Plugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; +import { SecurityPlugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; @@ -16,13 +16,13 @@ import { taskManagerMock } from '../../task_manager/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; describe('Security Plugin', () => { - let plugin: Plugin; + let plugin: SecurityPlugin; let mockCoreSetup: ReturnType; let mockCoreStart: ReturnType; let mockSetupDependencies: PluginSetupDependencies; let mockStartDependencies: PluginStartDependencies; beforeEach(() => { - plugin = new Plugin( + plugin = new SecurityPlugin( coreMock.createPluginInitializerContext( ConfigSchema.validate({ session: { idleTimeout: 1500 }, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 3156af7e930bd..cccfa7de6d177 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -8,6 +8,7 @@ import { combineLatest, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { SecurityOssPluginSetup } from 'src/plugins/security_oss/server'; import { @@ -16,6 +17,7 @@ import { KibanaRequest, Logger, PluginInitializerContext, + Plugin, } from '../../../../src/core/server'; import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; @@ -101,7 +103,13 @@ export interface PluginStartDependencies { /** * Represents Security Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class SecurityPlugin + implements + Plugin< + RecursiveReadonly, + RecursiveReadonly, + PluginSetupDependencies + > { private readonly logger: Logger; private authorizationSetup?: AuthorizationServiceSetup; private auditSetup?: AuditServiceSetup; diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index 3791c63f662ae..4658e6774b726 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from '../../../../src/core/server'; import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX } from '../common/constants'; @@ -38,9 +37,7 @@ export const configSchema = schema.object({ validateArtifactDownloads: schema.boolean({ defaultValue: true }), }); -export const createConfig$ = (context: PluginInitializerContext) => - context.config.create>(); +export const createConfig = (context: PluginInitializerContext) => + context.config.get>(); -export type ConfigType = ReturnType extends Observable - ? T - : ReturnType; +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 0d5d83582b42b..8c35fd2ce8f8b 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -6,7 +6,6 @@ */ import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import LRU from 'lru-cache'; @@ -47,7 +46,7 @@ import { isNotificationAlertExecutor } from './lib/detection_engine/notification import { ManifestTask } from './endpoint/lib/artifacts'; import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { AppClientFactory } from './client'; -import { createConfig$, ConfigType } from './config'; +import { createConfig, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; import { APP_ID, @@ -119,8 +118,7 @@ const securitySubPlugins = [ export class Plugin implements IPlugin { private readonly logger: Logger; - private readonly config$: Observable; - private config?: ConfigType; + private readonly config: ConfigType; private context: PluginInitializerContext; private appClientFactory: AppClientFactory; private setupPlugins?: SetupPlugins; @@ -137,7 +135,7 @@ export class Plugin implements IPlugin({ max: 3, maxAge: 1000 * 60 * 5 }); @@ -146,13 +144,12 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('plugin setup'); this.setupPlugins = plugins; - const config = await this.config$.pipe(first()).toPromise(); - this.config = config; - const globalConfig = await this.context.config.legacy.globalConfig$.pipe(first()).toPromise(); + const config = this.config; + const globalConfig = this.context.config.legacy.get(); initSavedObjects(core.savedObjects); initUiSettings(core.uiSettings); diff --git a/x-pack/plugins/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index 9d4614cf60294..c93b5dbc4c36d 100644 --- a/x-pack/plugins/snapshot_restore/server/plugin.ts +++ b/x-pack/plugins/snapshot_restore/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { CoreSetup, @@ -43,14 +42,11 @@ export class SnapshotRestoreServerPlugin implements Plugin this.license = new License(); } - public async setup( + public setup( { http, getStartServices }: CoreSetup, { licensing, features, security, cloud }: Dependencies - ): Promise { - const pluginConfig = await this.context.config - .create() - .pipe(first()) - .toPromise(); + ): void { + const pluginConfig = this.context.config.get(); if (!pluginConfig.enabled) { return; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 9d2a075dc35f9..fb6c00c2f6f48 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -7,7 +7,7 @@ import type { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema, spacesConfigDeprecationProvider } from './config'; -import { Plugin } from './plugin'; +import { SpacesPlugin } from './plugin'; // These exports are part of public Spaces plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. Ideally we should @@ -32,4 +32,4 @@ export const config: PluginConfigDescriptor = { deprecations: spacesConfigDeprecationProvider, }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new SpacesPlugin(initializerContext); diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index d576858c98e36..d1bf4d51700ba 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -9,7 +9,7 @@ import { CoreSetup } from 'src/core/server'; import { coreMock } from 'src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; -import { Plugin, PluginsStart } from './plugin'; +import { SpacesPlugin, PluginsStart } from './plugin'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; describe('Spaces Plugin', () => { @@ -20,7 +20,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); const spacesSetup = plugin.setup(core, { features, licensing }); expect(spacesSetup).toMatchInlineSnapshot(` Object { @@ -43,7 +43,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing }); @@ -59,7 +59,7 @@ describe('Spaces Plugin', () => { const usageCollection = usageCollectionPluginMock.createSetupContract(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing, usageCollection }); @@ -72,7 +72,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing }); @@ -99,7 +99,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(coreSetup, { features, licensing }); const coreStart = coreMock.createStart(); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index d9d32dd68c95d..4b26b1016d530 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -13,6 +13,7 @@ import { CoreStart, Logger, PluginInitializerContext, + Plugin, } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup, @@ -62,7 +63,8 @@ export interface SpacesPluginStart { spacesService: SpacesServiceStart; } -export class Plugin { +export class SpacesPlugin + implements Plugin { private readonly config$: Observable; private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; diff --git a/x-pack/plugins/stack_alerts/server/plugin.ts b/x-pack/plugins/stack_alerts/server/plugin.ts index 261d3d51aeb80..1343c46ecdd72 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.ts @@ -19,10 +19,7 @@ export class AlertingBuiltinsPlugin this.logger = ctx.logger.get(); } - public async setup( - core: CoreSetup, - { alerts, features }: StackAlertsDeps - ): Promise { + public setup(core: CoreSetup, { alerts, features }: StackAlertsDeps) { features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE); registerBuiltInAlertTypes({ @@ -34,6 +31,6 @@ export class AlertingBuiltinsPlugin }); } - public async start(): Promise {} - public async stop(): Promise {} + public start() {} + public stop() {} } diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 77031d4764968..0a879ce92cba6 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -39,7 +39,7 @@ describe('TaskManagerPlugin', () => { pluginInitializerContext.env.instanceUuid = ''; const taskManagerPlugin = new TaskManagerPlugin(pluginInitializerContext); - expect(taskManagerPlugin.setup(coreMock.createSetup())).rejects.toEqual( + expect(() => taskManagerPlugin.setup(coreMock.createSetup())).toThrow( new Error(`TaskManager is unable to start as Kibana has no valid UUID assigned to it.`) ); }); diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 62b8b75a38d73..149d111b08f02 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -6,7 +6,7 @@ */ import { combineLatest, Observable, Subject } from 'rxjs'; -import { first, map, distinctUntilChanged } from 'rxjs/operators'; +import { map, distinctUntilChanged } from 'rxjs/operators'; import { PluginInitializerContext, Plugin, @@ -46,7 +46,7 @@ export class TaskManagerPlugin implements Plugin { private taskPollingLifecycle?: TaskPollingLifecycle; private taskManagerId?: string; - private config?: TaskManagerConfig; + private config: TaskManagerConfig; private logger: Logger; private definitions: TaskTypeDictionary; private middleware: Middleware = createInitialMiddleware(); @@ -56,15 +56,11 @@ export class TaskManagerPlugin constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; this.logger = initContext.logger.get(); + this.config = initContext.config.get(); this.definitions = new TaskTypeDictionary(this.logger); } - public async setup(core: CoreSetup): Promise { - this.config = await this.initContext.config - .create() - .pipe(first()) - .toPromise(); - + public setup(core: CoreSetup): TaskManagerSetupContract { this.elasticsearchAndSOAvailability$ = getElasticsearchAndSOAvailability(core.status.core$); setupSavedObjects(core.savedObjects, this.config); diff --git a/x-pack/plugins/triggers_actions_ui/server/plugin.ts b/x-pack/plugins/triggers_actions_ui/server/plugin.ts index f7c7e48d93d08..3933751105cb4 100644 --- a/x-pack/plugins/triggers_actions_ui/server/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/server/plugin.ts @@ -30,7 +30,7 @@ export class TriggersActionsPlugin implements Plugin this.data = getService(); } - public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { + public setup(core: CoreSetup, plugins: PluginsSetup): void { const router = core.http.createRouter(); registerDataService({ logger: this.logger, @@ -42,7 +42,7 @@ export class TriggersActionsPlugin implements Plugin createHealthRoute(this.logger, router, BASE_ROUTE, plugins.alerts !== undefined); } - public async start(): Promise { + public start(): PluginStartContract { return { data: this.data, }; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index d7c0a465dd3e0..8bbbecf8108fe 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -50,10 +50,7 @@ export class UptimePlugin implements Plugin { constructor(_context: PluginInitializerContext) {} - public async setup( - core: CoreSetup, - plugins: ClientPluginsSetup - ): Promise { + public setup(core: CoreSetup, plugins: ClientPluginsSetup): void { if (plugins.home) { plugins.home.featureCatalogue.register({ id: PLUGIN.ID, diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 220a814835e75..ceade131fc5af 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -47,7 +47,7 @@ export class WatcherServerPlugin implements Plugin { this.log = ctx.logger.get(); } - async setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { + setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { const router = http.createRouter(); const routeDependencies: RouteDependencies = { router, diff --git a/x-pack/plugins/xpack_legacy/server/plugin.ts b/x-pack/plugins/xpack_legacy/server/plugin.ts index 9bd42171c75d5..ffef7117bbbd8 100644 --- a/x-pack/plugins/xpack_legacy/server/plugin.ts +++ b/x-pack/plugins/xpack_legacy/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; - import { CoreStart, CoreSetup, @@ -23,11 +21,9 @@ interface SetupPluginDeps { export class XpackLegacyPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) {} - public async setup(core: CoreSetup, { usageCollection }: SetupPluginDeps) { + public setup(core: CoreSetup, { usageCollection }: SetupPluginDeps) { const router = core.http.createRouter(); - const globalConfig = await this.initContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initContext.config.legacy.get(); const serverInfo = core.http.getServerInfo(); registerSettingsRoute({ From d152723bb1a1ea788e489dc29970fca23ae46702 Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 8 Feb 2021 14:13:20 +0200 Subject: [PATCH 002/100] [Data Table] Add unit tests (#90173) * Move formatting columns into response handler * Use shared csv export * Cleanup files * Fix type * Fix translation * Filter out non-dimension values * Add unit tests for tableVisResponseHandler * Add unit tests for createFormattedTable * Add unit tests for addPercentageColumn * Add unit tests for usePagination * Add unit tests for useUiState * Add unit tests for table visualization * Add unit tests for TableVisBasic * Add unit tests for cell * Update license Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../table_vis_basic.test.tsx.snap | 115 +++++++++ .../table_vis_cell.test.tsx.snap | 13 ++ .../components/table_vis_basic.test.tsx | 130 +++++++++++ .../public/components/table_vis_cell.test.tsx | 36 +++ .../components/table_visualization.test.tsx | 69 ++++++ .../utils/add_percentage_column.test.ts | 79 +++++++ .../utils/create_formatted_table.test.ts | 218 ++++++++++++++++++ .../utils/table_vis_response_handler.test.ts | 171 ++++++++++++++ .../utils/table_vis_response_handler.ts | 5 +- .../public/utils/use/use_pagination.test.ts | 119 ++++++++++ .../public/utils/use/use_ui_state.test.ts | 163 +++++++++++++ 11 files changed, 1115 insertions(+), 3 deletions(-) create mode 100644 src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap create mode 100644 src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap create mode 100644 src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_visualization.test.tsx create mode 100644 src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts diff --git a/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap new file mode 100644 index 0000000000000..85cf9422630d6 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TableVisBasic should init data grid 1`] = ` + + + +`; + +exports[`TableVisBasic should init data grid with title provided - for split mode 1`] = ` + + +

+ My data table +

+
+ +
+`; + +exports[`TableVisBasic should render the toolbar 1`] = ` + + , + "showColumnSelector": false, + "showFullScreenSelector": false, + "showSortSelector": false, + "showStyleSelector": false, + } + } + /> + +`; diff --git a/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap new file mode 100644 index 0000000000000..b380b85f7f356 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`table vis cell should return a cell component with data in scope 1`] = ` +
+`; diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx new file mode 100644 index 0000000000000..0fb74a41b5df0 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { TableVisBasic } from './table_vis_basic'; +import { FormattedColumn, TableVisConfig, TableVisUiState } from '../types'; +import { DatatableColumn } from 'src/plugins/expressions'; +import { createTableVisCell } from './table_vis_cell'; +import { createGridColumns } from './table_vis_columns'; + +jest.mock('./table_vis_columns', () => ({ + createGridColumns: jest.fn(() => []), +})); +jest.mock('./table_vis_cell', () => ({ + createTableVisCell: jest.fn(() => () => {}), +})); + +describe('TableVisBasic', () => { + const props = { + fireEvent: jest.fn(), + table: { + columns: [], + rows: [], + formattedColumns: { + test: { + formattedTotal: 100, + } as FormattedColumn, + }, + }, + visConfig: {} as TableVisConfig, + uiStateProps: { + sort: { + columnIndex: null, + direction: null, + }, + columnsWidth: [], + setColumnsWidth: jest.fn(), + setSort: jest.fn(), + }, + }; + + it('should init data grid', () => { + const comp = shallow(); + expect(comp).toMatchSnapshot(); + }); + + it('should init data grid with title provided - for split mode', () => { + const title = 'My data table'; + const comp = shallow(); + expect(comp).toMatchSnapshot(); + }); + + it('should render the toolbar', () => { + const comp = shallow( + + ); + expect(comp).toMatchSnapshot(); + }); + + it('should sort rows by column and pass the sorted rows for consumers', () => { + const uiStateProps = { + ...props.uiStateProps, + sort: { + columnIndex: 1, + direction: 'desc', + } as TableVisUiState['sort'], + }; + const table = { + columns: [{ id: 'first' }, { id: 'second' }] as DatatableColumn[], + rows: [ + { first: 1, second: 2 }, + { first: 3, second: 4 }, + { first: 5, second: 6 }, + ], + formattedColumns: {}, + }; + const sortedRows = [ + { first: 5, second: 6 }, + { first: 3, second: 4 }, + { first: 1, second: 2 }, + ]; + const comp = shallow( + + ); + expect(createTableVisCell).toHaveBeenCalledWith(sortedRows, table.formattedColumns); + expect(createGridColumns).toHaveBeenCalledWith( + table.columns, + sortedRows, + table.formattedColumns, + uiStateProps.columnsWidth, + props.fireEvent + ); + + const { onSort } = comp.find('EuiDataGrid').prop('sorting'); + // sort the first column + onSort([{ id: 'first', direction: 'asc' }]); + expect(uiStateProps.setSort).toHaveBeenCalledWith({ columnIndex: 0, direction: 'asc' }); + // sort the second column - should erase the first column sorting since there is only one level sorting available + onSort([ + { id: 'first', direction: 'asc' }, + { id: 'second', direction: 'desc' }, + ]); + expect(uiStateProps.setSort).toHaveBeenCalledWith({ columnIndex: 1, direction: 'desc' }); + }); + + it('should pass renderFooterCellValue for the total row', () => { + const comp = shallow( + + ); + const renderFooterCellValue: (props: any) => void = comp + .find('EuiDataGrid') + .prop('renderFooterCellValue'); + expect(renderFooterCellValue).toEqual(expect.any(Function)); + expect(renderFooterCellValue({ columnId: 'test' })).toEqual(100); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx new file mode 100644 index 0000000000000..322ceacbe002e --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { createTableVisCell } from './table_vis_cell'; +import { FormattedColumns } from '../types'; + +describe('table vis cell', () => { + it('should return a cell component with data in scope', () => { + const rows = [{ first: 1, second: 2 }]; + const formattedColumns = ({ + second: { + formatter: { + convert: jest.fn(), + }, + }, + } as unknown) as FormattedColumns; + const Cell = createTableVisCell(rows, formattedColumns); + const cellProps = { + rowIndex: 0, + columnId: 'second', + } as EuiDataGridCellValueElementProps; + + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + expect(formattedColumns.second.formatter.convert).toHaveBeenLastCalledWith(2, 'html'); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_visualization.test.tsx b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx new file mode 100644 index 0000000000000..3d169531f5757 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('../utils', () => ({ + useUiState: jest.fn(() => 'uiState'), +})); + +import React from 'react'; +import { shallow } from 'enzyme'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { coreMock } from '../../../../core/public/mocks'; +import { TableVisConfig, TableVisData } from '../types'; +import TableVisualizationComponent from './table_visualization'; +import { useUiState } from '../utils'; + +describe('TableVisualizationComponent', () => { + const coreStartMock = coreMock.createStart(); + const handlers = ({ + done: jest.fn(), + uiState: 'uiState', + event: 'event', + } as unknown) as IInterpreterRenderHandlers; + const visData: TableVisData = { + table: { + columns: [], + rows: [], + formattedColumns: {}, + }, + tables: [], + }; + const visConfig = ({} as unknown) as TableVisConfig; + + it('should render the basic table', () => { + const comp = shallow( + + ); + expect(useUiState).toHaveBeenLastCalledWith(handlers.uiState); + expect(comp.find('.tbvChart__splitColumns').exists()).toBeFalsy(); + expect(comp.find('.tbvChart__split').exists()).toBeTruthy(); + }); + + it('should render split table', () => { + const comp = shallow( + + ); + expect(useUiState).toHaveBeenLastCalledWith(handlers.uiState); + expect(comp.find('.tbvChart__splitColumns').exists()).toBeTruthy(); + expect(comp.find('.tbvChart__split').exists()).toBeFalsy(); + expect(comp.find('[data-test-subj="tbvChart"]').children().prop('tables')).toEqual([]); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts b/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts new file mode 100644 index 0000000000000..0280637acc099 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: jest.fn(() => 'formatter'), + })), +})); + +import { FieldFormat } from 'src/plugins/data/public'; +import { TableContext } from '../types'; +import { addPercentageColumn } from './add_percentage_column'; + +describe('', () => { + const table: TableContext = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-5', name: 'category.keyword: Descending', meta: { type: 'string' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + { 'col-0-1': 6, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + ], + formattedColumns: { + 'col-0-1': { + sumTotal: 7, + title: 'Count', + filterable: false, + formatter: {} as FieldFormat, + }, + }, + }; + + it('should dnot add percentage column if it was not found', () => { + const output = addPercentageColumn(table, 'Extra'); + expect(output).toBe(table); + }); + + it('should add a brand new percentage column into table based on data', () => { + const output = addPercentageColumn(table, 'Count'); + const expectedColumns = [ + table.columns[0], + { + id: 'col-0-1-percents', + meta: { + params: { + id: 'percent', + }, + type: 'number', + }, + name: 'Count percentages', + }, + table.columns[1], + table.columns[2], + ]; + const expectedRows = [ + { ...table.rows[0], 'col-0-1-percents': 0.14285714285714285 }, + { ...table.rows[1], 'col-0-1-percents': 0.8571428571428571 }, + ]; + expect(output).toEqual({ + columns: expectedColumns, + rows: expectedRows, + formattedColumns: { + ...table.formattedColumns, + 'col-0-1-percents': { + filterable: false, + formatter: 'formatter', + title: 'Count percentages', + }, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts b/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts new file mode 100644 index 0000000000000..0a9c7320d4359 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const mockDeserialize = jest.fn(() => ({})); + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: mockDeserialize, + })), +})); + +import { Datatable } from 'src/plugins/expressions'; +import { AggTypes } from '../../common'; +import { TableVisConfig } from '../types'; +import { createFormattedTable } from './create_formatted_table'; + +const visConfig: TableVisConfig = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + title: 'My data table', + dimensions: { + buckets: [ + { + accessor: 1, + aggType: 'terms', + format: { id: 'string' }, + label: 'category_keyword: Descending', + params: {}, + }, + ], + metrics: [ + { accessor: 0, aggType: 'count', format: { id: 'number' }, label: 'Count', params: {} }, + ], + }, +}; + +describe('createFormattedTable', () => { + const table: Datatable = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-5', name: 'category.keyword: Descending', meta: { type: 'string' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + { 'col-0-1': 6, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + ], + type: 'datatable', + }; + + it('should create formatted columns from data response and flter out non-dimension columns', () => { + const output = createFormattedTable(table, visConfig); + + // column to split is filtered out of real data representing + expect(output.columns).toEqual([table.columns[0], table.columns[1]]); + expect(output.rows).toEqual(table.rows); + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: {}, + title: 'Count', + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add total sum to numeric columns', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, visConfig); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 7, + formattedTotal: 7, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add total average to numeric columns', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.AVG }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 3.5, + formattedTotal: 3.5, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should find min value as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.MIN }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 1, + formattedTotal: 1, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should find max value as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.MAX }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 6, + formattedTotal: 6, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add rows count as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.COUNT }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 2, + formattedTotal: 2, + }, + 'col-1-5': { + filterable: true, + formattedTotal: 2, + formatter: {}, + sumTotal: "0Women's ClothingWomen's Clothing", + title: 'category.keyword: Descending', + total: 2, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts new file mode 100644 index 0000000000000..8adc535e802f0 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const mockConverter = jest.fn((name) => `By ${name}`); + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: jest.fn(() => ({ + convert: mockConverter, + })), + })), +})); + +jest.mock('./create_formatted_table', () => ({ + createFormattedTable: jest.fn((data) => ({ + ...data, + formattedColumns: {}, + })), +})); + +jest.mock('./add_percentage_column', () => ({ + addPercentageColumn: jest.fn((data, column) => ({ + ...data, + percentage: `${column} with percentage`, + })), +})); + +import { Datatable } from 'src/plugins/expressions'; +import { SchemaConfig } from 'src/plugins/visualizations/public'; +import { AggTypes } from '../../common'; +import { TableGroup, TableVisConfig } from '../types'; +import { addPercentageColumn } from './add_percentage_column'; +import { createFormattedTable } from './create_formatted_table'; +import { tableVisResponseHandler } from './table_vis_response_handler'; + +const visConfig: TableVisConfig = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.AVG, + percentageCol: '', + title: 'My data table', + dimensions: { + buckets: [], + metrics: [], + }, +}; + +describe('tableVisResponseHandler', () => { + describe('basic table', () => { + const input: Datatable = { + columns: [], + rows: [], + type: 'datatable', + }; + + it('should create formatted table for basic usage', () => { + const output = tableVisResponseHandler(input, visConfig); + + expect(output.direction).toBeUndefined(); + expect(output.tables.length).toEqual(0); + expect(addPercentageColumn).not.toHaveBeenCalled(); + expect(createFormattedTable).toHaveBeenCalledWith(input, visConfig); + expect(output.table).toEqual({ + ...input, + formattedColumns: {}, + }); + }); + + it('should add a percentage column if it is set', () => { + const output = tableVisResponseHandler(input, { ...visConfig, percentageCol: 'Count' }); + expect(output.table).toEqual({ + ...input, + formattedColumns: {}, + percentage: 'Count with percentage', + }); + }); + }); + + describe('split table', () => { + const input: Datatable = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-2': 'Men' }, + { 'col-0-1': 3, 'col-1-2': 'Women' }, + { 'col-0-1': 6, 'col-1-2': 'Men' }, + ], + type: 'datatable', + }; + const split: SchemaConfig[] = [ + { + accessor: 1, + label: 'Split', + format: {}, + params: {}, + aggType: 'terms', + }, + ]; + const expectedOutput: TableGroup[] = [ + { + title: 'By Men: Gender', + table: { + columns: input.columns, + rows: [input.rows[0], input.rows[2]], + formattedColumns: {}, + }, + }, + { + title: 'By Women: Gender', + table: { + columns: input.columns, + rows: [input.rows[1]], + formattedColumns: {}, + }, + }, + ]; + + it('should split data by row', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + dimensions: { ...visConfig.dimensions, splitRow: split }, + }); + + expect(output.direction).toEqual('row'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual(expectedOutput); + }); + + it('should split data by column', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + dimensions: { ...visConfig.dimensions, splitColumn: split }, + }); + + expect(output.direction).toEqual('column'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual(expectedOutput); + }); + + it('should add percentage columns to each table', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + percentageCol: 'Count', + dimensions: { ...visConfig.dimensions, splitColumn: split }, + }); + + expect(output.direction).toEqual('column'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual([ + { + ...expectedOutput[0], + table: { ...expectedOutput[0].table, percentage: 'Count with percentage' }, + }, + { + ...expectedOutput[1], + table: { ...expectedOutput[1].table, percentage: 'Count with percentage' }, + }, + ]); + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts index e0919671135ea..69521c20cddfe 100644 --- a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts +++ b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts @@ -27,7 +27,6 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon const splitColumnIndex = split[0].accessor; const splitColumnFormatter = getFormatService().deserialize(split[0].format); const splitColumn = input.columns[splitColumnIndex]; - const columns = input.columns.filter((c, idx) => idx !== splitColumnIndex); const splitMap: { [key: string]: number } = {}; let splitIndex = 0; @@ -39,7 +38,7 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon const tableGroup: TableGroup = { title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, table: { - columns, + columns: input.columns, rows: [], formattedColumns: {}, }, @@ -53,7 +52,7 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon }); tables.forEach((tg) => { - tg.table = createFormattedTable({ ...tg.table, columns: input.columns }, visConfig); + tg.table = createFormattedTable(tg.table, visConfig); if (visConfig.percentageCol) { tg.table = addPercentageColumn(tg.table, visConfig.percentageCol); diff --git a/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts b/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts new file mode 100644 index 0000000000000..3d0b58aa6c8a3 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { AggTypes } from '../../../common'; +import { usePagination } from './use_pagination'; + +describe('usePagination', () => { + const visParams = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + title: 'My data table', + }; + + it('should set up pagination on init', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should skip setting the pagination if perPage is not set', () => { + const { result } = renderHook(() => usePagination({ ...visParams, perPage: '' }, 15)); + + expect(result.current).toBeUndefined(); + }); + + it('should change the page via callback', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + act(() => { + // change the page to the next one + result.current?.onChangePage(1); + }); + + expect(result.current).toEqual({ + pageIndex: 1, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should change items per page via callback', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + act(() => { + // change the page to the next one + result.current?.onChangeItemsPerPage(20); + }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 20, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should change the page when props were changed', () => { + const { result, rerender } = renderHook( + (props) => usePagination(props.visParams, props.rowCount), + { + initialProps: { + visParams, + rowCount: 15, + }, + } + ); + const updatedParams = { ...visParams, perPage: 5 }; + + // change items per page count + rerender({ visParams: updatedParams, rowCount: 15 }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + + act(() => { + // change the page to the last one - 3 + result.current?.onChangePage(3); + }); + + expect(result.current).toEqual({ + pageIndex: 3, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + + // decrease the rows count + rerender({ visParams: updatedParams, rowCount: 10 }); + + // should switch to the last available page + expect(result.current).toEqual({ + pageIndex: 1, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts b/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts new file mode 100644 index 0000000000000..be1f9d3a10cf7 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import type { PersistedState } from 'src/plugins/visualizations/public'; +import { TableVisUiState } from '../../types'; +import { useUiState } from './use_ui_state'; + +describe('useUiState', () => { + let uiState: PersistedState; + + beforeEach(() => { + uiState = { + get: jest.fn(), + on: jest.fn(), + off: jest.fn(), + set: jest.fn(), + } as any; + }); + + it("should init default columnsWidth & sort if uiState doesn't have it set", () => { + const { result } = renderHook(() => useUiState(uiState)); + + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: null, + direction: null, + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + }); + + it('should subscribe on uiState changes and update local state', async () => { + const { result, unmount, waitForNextUpdate } = renderHook(() => useUiState(uiState)); + + expect(uiState.on).toHaveBeenCalledWith('change', expect.any(Function)); + // @ts-expect-error + const updateOnChange = uiState.on.mock.calls[0][1]; + + uiState.getChanges = jest.fn(() => ({ + vis: { + params: { + sort: { + columnIndex: 1, + direction: 'asc', + }, + colWidth: [], + }, + }, + })); + + act(() => { + updateOnChange(); + }); + + await waitForNextUpdate(); + + // should update local state with new values + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: 1, + direction: 'asc', + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + + act(() => { + updateOnChange(); + }); + + // should skip setting the state again if it is equal + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: 1, + direction: 'asc', + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + + unmount(); + + expect(uiState.off).toHaveBeenCalledWith('change', updateOnChange); + }); + + describe('updating uiState through callbacks', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + it('should update the uiState with new sort', async () => { + const { result } = renderHook(() => useUiState(uiState)); + const newSort: TableVisUiState['sort'] = { + columnIndex: 5, + direction: 'desc', + }; + + act(() => { + result.current.setSort(newSort); + }); + + expect(result.current.sort).toEqual(newSort); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(1); + expect(uiState.set).toHaveBeenCalledWith('vis.params.sort', newSort); + }); + + it('should update the uiState with new columns width', async () => { + const { result } = renderHook(() => useUiState(uiState)); + const col1 = { colIndex: 0, width: 300 }; + const col2 = { colIndex: 1, width: 100 }; + + // set width of a column + act(() => { + result.current.setColumnsWidth(col1); + }); + + expect(result.current.columnsWidth).toEqual([col1]); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(1); + expect(uiState.set).toHaveBeenLastCalledWith('vis.params.colWidth', [col1]); + + // set width of another column + act(() => { + result.current.setColumnsWidth(col2); + }); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(2); + expect(uiState.set).toHaveBeenLastCalledWith('vis.params.colWidth', [col1, col2]); + + const updatedCol1 = { colIndex: 0, width: 200 }; + // update width of existing column + act(() => { + result.current.setColumnsWidth(updatedCol1); + }); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(3); + expect(uiState.set).toHaveBeenCalledWith('vis.params.colWidth', [updatedCol1, col2]); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + }); +}); From 4a1946b7ae980181b3becb021baec2f18f9373ed Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 8 Feb 2021 13:48:18 +0100 Subject: [PATCH 003/100] [Lens] Retain column config (#90048) --- .../visualization.test.tsx | 34 +++++++++++++++++++ .../datatable_visualization/visualization.tsx | 12 ++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 25275ba8e2249..2a6228f16867d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -108,6 +108,40 @@ describe('Datatable Visualization', () => { expect(suggestions.length).toBeGreaterThan(0); }); + it('should retain width and hidden config from existing state', () => { + const suggestions = datatableVisualization.getSuggestions({ + state: { + layerId: 'first', + columns: [ + { columnId: 'col1', width: 123 }, + { columnId: 'col2', hidden: true }, + ], + sorting: { + columnId: 'col1', + direction: 'asc', + }, + }, + table: { + isMultiRow: true, + layerId: 'first', + changeType: 'initial', + columns: [numCol('col1'), strCol('col2'), strCol('col3')], + }, + keptLayerIds: [], + }); + + expect(suggestions.length).toBeGreaterThan(0); + expect(suggestions[0].state.columns).toEqual([ + { columnId: 'col1', width: 123 }, + { columnId: 'col2', hidden: true }, + { columnId: 'col3' }, + ]); + expect(suggestions[0].state.sorting).toEqual({ + columnId: 'col1', + direction: 'asc', + }); + }); + it('should not make suggestions when the table is unchanged', () => { const suggestions = datatableVisualization.getSuggestions({ state: { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 77fda43c37fef..9625a814c7958 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -98,6 +98,12 @@ export const datatableVisualization: Visualization ) { return []; } + const oldColumnSettings: Record = {}; + if (state) { + state.columns.forEach((column) => { + oldColumnSettings[column.columnId] = column; + }); + } const title = table.changeType === 'unchanged' ? i18n.translate('xpack.lens.datatable.suggestionLabel', { @@ -126,8 +132,12 @@ export const datatableVisualization: Visualization // table with >= 10 columns will have a score of 0.4, fewer columns reduce score score: (Math.min(table.columns.length, 10) / 10) * 0.4, state: { + ...(state || {}), layerId: table.layerId, - columns: table.columns.map((col) => ({ columnId: col.columnId })), + columns: table.columns.map((col) => ({ + ...(oldColumnSettings[col.columnId] || {}), + columnId: col.columnId, + })), }, previewIcon: LensIconChartDatatable, // tables are hidden from suggestion bar, but used for drag & drop and chart switching From be9f7c3dc99658d315dd21963aaf243a34a85e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 8 Feb 2021 14:37:32 +0100 Subject: [PATCH 004/100] [APM] Export ProcessorEvent type (#90540) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/server/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index f0524e67d324c..da3afd03513f0 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -97,3 +97,4 @@ export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); export { APMPlugin, APMPluginSetup } from './plugin'; +export type { ProcessorEvent } from '../common/processor_event'; From d201ed756f79bae54e1d4b03c44ecdf3ea3bdda6 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Mon, 8 Feb 2021 13:40:41 +0000 Subject: [PATCH 005/100] [APM-UI][E2E] use githubNotify step (#90514) --- .ci/end2end.groovy | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 025836a90204c..a89ff166bf32e 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -121,9 +121,15 @@ pipeline { } def notifyStatus(String description, String status) { - withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('pipeline')) + notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('pipeline')) } def notifyTestStatus(String description, String status) { - withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('tests')) + notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('tests')) +} + +def notify(Map args = [:]) { + retryWithSleep(retries: 2, seconds: 5, backoff: true) { + githubNotify(context: args.context, description: args.description, status: args.status, targetUrl: args.targetUrl) + } } From 3d2373325c76dc595d53f78209ff98b86e57ff81 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 8 Feb 2021 15:34:19 +0100 Subject: [PATCH 006/100] [Discover] Inline state modifying function to react (#89543) --- .../public/application/angular/discover.js | 111 +--------- .../application/angular/discover_legacy.html | 11 - .../angular/doc_table/actions/columns.ts | 61 ++++++ .../components/create_discover_directive.ts | 10 - .../application/components/discover.test.tsx | 25 +-- .../application/components/discover.tsx | 205 ++++++++++-------- .../components/discover_topnav.test.tsx | 68 ++++++ .../components/discover_topnav.tsx | 71 ++++++ .../sidebar/discover_index_pattern.test.tsx | 40 ++-- .../sidebar/discover_index_pattern.tsx | 60 ++++- .../sidebar/discover_sidebar.test.tsx | 8 +- .../components/sidebar/discover_sidebar.tsx | 107 ++------- .../discover_sidebar_responsive.test.tsx | 22 +- .../sidebar/discover_sidebar_responsive.tsx | 26 ++- .../public/application/components/types.ts | 55 +---- .../application/helpers/popularize_field.ts | 4 +- 16 files changed, 480 insertions(+), 404 deletions(-) create mode 100644 src/plugins/discover/public/application/components/discover_topnav.test.tsx create mode 100644 src/plugins/discover/public/application/components/discover_topnav.tsx diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index b22bb6dc71342..af63485507d05 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -22,7 +22,6 @@ import { syncQueryStateWithUrl, } from '../../../../data/public'; import { getSortArray } from './doc_table'; -import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; import { discoverResponseHandler } from './response_handler'; @@ -43,13 +42,9 @@ import { setBreadcrumbsTitle, } from '../helpers/breadcrumbs'; import { validateTimeRange } from '../helpers/validate_time_range'; -import { popularizeField } from '../helpers/popularize_field'; -import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state'; import { addFatalError } from '../../../../kibana_legacy/public'; -import { METRIC_TYPE } from '@kbn/analytics'; import { DEFAULT_COLUMNS_SETTING, - MODIFY_COLUMNS_ON_SWITCH, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING, @@ -69,12 +64,10 @@ const { chrome, data, history: getHistory, - indexPatterns, filterManager, timefilter, toastNotifications, uiSettings: config, - trackUiMetric, } = getServices(); const fetchStatuses = { @@ -292,21 +285,6 @@ function discoverController($route, $scope, Promise) { } ); - $scope.setIndexPattern = async (id) => { - const nextIndexPattern = await indexPatterns.get(id); - if (nextIndexPattern) { - const nextAppState = getSwitchIndexPatternAppState( - $scope.indexPattern, - nextIndexPattern, - $scope.state.columns, - $scope.state.sort, - config.get(MODIFY_COLUMNS_ON_SWITCH), - $scope.useNewFieldsApi - ); - await setAppState(nextAppState); - } - }; - // update data source when filters update subscriptions.add( subscribeWithScope( @@ -327,6 +305,7 @@ function discoverController($route, $scope, Promise) { sampleSize: config.get(SAMPLE_SIZE_SETTING), timefield: getTimeField(), savedSearch: savedSearch, + services, indexPatternList: $route.current.locals.savedObjects.ip.list, config: config, setHeaderActionMenu: getHeaderActionMenuMounter(), @@ -340,18 +319,8 @@ function discoverController($route, $scope, Promise) { requests: new RequestAdapter(), }); - $scope.timefilterUpdateHandler = (ranges) => { - timefilter.setTime({ - from: moment(ranges.from).toISOString(), - to: moment(ranges.to).toISOString(), - mode: 'absolute', - }); - }; $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; - $scope.showSaveQuery = capabilities.discover.saveQuery; - $scope.showTimeCol = - !config.get('doc_table:hideTimeColumn', false) && $scope.indexPattern.timeFieldName; let abortController; $scope.$on('$destroy', () => { @@ -495,12 +464,6 @@ function discoverController($route, $scope, Promise) { ) ); - $scope.changeInterval = (interval) => { - if (interval) { - setAppState({ interval }); - } - }; - $scope.$watchMulti( ['rows', 'fetchStatus'], (function updateResultState() { @@ -606,19 +569,6 @@ function discoverController($route, $scope, Promise) { } }; - $scope.updateSavedQueryId = (newSavedQueryId) => { - if (newSavedQueryId) { - setAppState({ savedQuery: newSavedQueryId }); - } else { - // remove savedQueryId from state - const state = { - ...appStateContainer.getState(), - }; - delete state.savedQuery; - appStateContainer.set(state); - } - }; - function getDimensions(aggs, timeRange) { const [metric, agg] = aggs; agg.params.timeRange = timeRange; @@ -752,65 +702,6 @@ function discoverController($route, $scope, Promise) { return Promise.resolve(); }; - $scope.setSortOrder = function setSortOrder(sort) { - setAppState({ sort }); - }; - - // TODO: On array fields, negating does not negate the combination, rather all terms - $scope.filterQuery = function (field, values, operation) { - const { indexPattern } = $scope; - - popularizeField(indexPattern, field.name, indexPatterns); - const newFilters = esFilters.generateFilters( - filterManager, - field, - values, - operation, - $scope.indexPattern.id - ); - if (trackUiMetric) { - trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); - } - return filterManager.addFilters(newFilters); - }; - - $scope.addColumn = function addColumn(columnName) { - const { indexPattern, useNewFieldsApi } = $scope; - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } - const columns = columnActions.addColumn($scope.state.columns, columnName, useNewFieldsApi); - setAppState({ columns }); - }; - - $scope.removeColumn = function removeColumn(columnName) { - const { indexPattern, useNewFieldsApi } = $scope; - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } - const columns = columnActions.removeColumn($scope.state.columns, columnName, useNewFieldsApi); - // The state's sort property is an array of [sortByColumn,sortDirection] - const sort = $scope.state.sort.length - ? $scope.state.sort.filter((subArr) => subArr[0] !== columnName) - : []; - setAppState({ columns, sort }); - }; - - $scope.moveColumn = function moveColumn(columnName, newIndex) { - const columns = columnActions.moveColumn($scope.state.columns, columnName, newIndex); - setAppState({ columns }); - }; - - $scope.setColumns = function setColumns(columns) { - // remove first element of columns if it's the configured timeFieldName, which is prepended automatically - const actualColumns = - $scope.indexPattern.timeFieldName && $scope.indexPattern.timeFieldName === columns[0] - ? columns.slice(1) - : columns; - $scope.state = { ...$scope.state, columns: actualColumns }; - setAppState({ columns: actualColumns }); - }; - async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages if (!getTimeField()) return; diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index 83a9cf23c85f3..dc18b7929318b 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -8,27 +8,16 @@ hits="hits" index-pattern="indexPattern" minimum-visible-rows="minimumVisibleRows" - on-add-column="addColumn" - on-add-filter="filterQuery" - on-move-column="moveColumn" - on-change-interval="changeInterval" - on-remove-column="removeColumn" - on-set-columns="setColumns" on-skip-bottom-button-click="onSkipBottomButtonClick" - on-sort="setSortOrder" opts="opts" reset-query="resetQuery" result-state="resultState" rows="rows" search-source="searchSource" - set-index-pattern="setIndexPattern" - show-save-query="showSaveQuery" state="state" - time-filter-update-handler="timefilterUpdateHandler" time-range="timeRange" top-nav-menu="topNavMenu" update-query="handleRefresh" - update-saved-query-id="updateSavedQueryId" use-new-fields-api="useNewFieldsApi" unmapped-fields-config="unmappedFieldsConfig" > diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts index 946f11024360f..53ced59b17c5d 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts @@ -5,6 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { Capabilities } from 'kibana/public'; +import { popularizeField } from '../../../helpers/popularize_field'; +import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services'; +import { AppState } from '../../discover_state'; /** * Helper function to provide a fallback to a single _source column if the given array of columns @@ -47,3 +51,60 @@ export function moveColumn(columns: string[], columnName: string, newIndex: numb modifiedColumns.splice(newIndex, 0, columnName); // insert before new index return modifiedColumns; } + +export function getStateColumnActions({ + capabilities, + indexPattern, + indexPatterns, + useNewFieldsApi, + setAppState, + state, +}: { + capabilities: Capabilities; + indexPattern: IndexPattern; + indexPatterns: IndexPatternsContract; + useNewFieldsApi: boolean; + setAppState: (state: Partial) => void; + state: AppState; +}) { + function onAddColumn(columnName: string) { + if (capabilities.discover.save) { + popularizeField(indexPattern, columnName, indexPatterns); + } + const columns = addColumn(state.columns || [], columnName, useNewFieldsApi); + setAppState({ columns }); + } + + function onRemoveColumn(columnName: string) { + if (capabilities.discover.save) { + popularizeField(indexPattern, columnName, indexPatterns); + } + const columns = removeColumn(state.columns || [], columnName, useNewFieldsApi); + // The state's sort property is an array of [sortByColumn,sortDirection] + const sort = + state.sort && state.sort.length + ? state.sort.filter((subArr) => subArr[0] !== columnName) + : []; + setAppState({ columns, sort }); + } + + function onMoveColumn(columnName: string, newIndex: number) { + const columns = moveColumn(state.columns || [], columnName, newIndex); + setAppState({ columns }); + } + + function onSetColumns(columns: string[]) { + // remove first element of columns if it's the configured timeFieldName, which is prepended automatically + const actualColumns = + indexPattern.timeFieldName && indexPattern.timeFieldName === columns[0] + ? columns.slice(1) + : columns; + setAppState({ columns: actualColumns }); + } + return { + onAddColumn, + onRemoveColumn, + onMoveColumn, + onSetColumns, + }; +} diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index 2a88c1b713132..8d1360aeaddad 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { Discover } from './discover'; export function createDiscoverDirective(reactDirective: any) { @@ -18,24 +17,15 @@ export function createDiscoverDirective(reactDirective: any) { ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], ['minimumVisibleRows', { watchDepth: 'reference' }], - ['onAddColumn', { watchDepth: 'reference' }], - ['onAddFilter', { watchDepth: 'reference' }], - ['onChangeInterval', { watchDepth: 'reference' }], - ['onMoveColumn', { watchDepth: 'reference' }], - ['onRemoveColumn', { watchDepth: 'reference' }], - ['onSetColumns', { watchDepth: 'reference' }], ['onSkipBottomButtonClick', { watchDepth: 'reference' }], - ['onSort', { watchDepth: 'reference' }], ['opts', { watchDepth: 'reference' }], ['resetQuery', { watchDepth: 'reference' }], ['resultState', { watchDepth: 'reference' }], ['rows', { watchDepth: 'reference' }], ['savedSearch', { watchDepth: 'reference' }], ['searchSource', { watchDepth: 'reference' }], - ['setIndexPattern', { watchDepth: 'reference' }], ['showSaveQuery', { watchDepth: 'reference' }], ['state', { watchDepth: 'reference' }], - ['timefilterUpdateHandler', { watchDepth: 'reference' }], ['timeRange', { watchDepth: 'reference' }], ['topNavMenu', { watchDepth: 'reference' }], ['updateQuery', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.test.tsx b/src/plugins/discover/public/application/components/discover.test.tsx index bb0014f4278a1..f0f11558abd65 100644 --- a/src/plugins/discover/public/application/components/discover.test.tsx +++ b/src/plugins/discover/public/application/components/discover.test.tsx @@ -11,6 +11,7 @@ import { shallowWithIntl } from '@kbn/test/jest'; import { Discover } from './discover'; import { esHits } from '../../__mocks__/es_hits'; import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { DiscoverServices } from '../../build_services'; import { GetStateReturn } from '../angular/discover_state'; import { savedSearchMock } from '../../__mocks__/saved_search'; import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; @@ -46,7 +47,14 @@ jest.mock('../../kibana_services', () => { function getProps(indexPattern: IndexPattern): DiscoverProps { const searchSourceMock = createSearchSourceMock({}); - const state = ({} as unknown) as GetStateReturn; + const services = ({ + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + } as unknown) as DiscoverServices; return { fetch: jest.fn(), @@ -56,14 +64,7 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { hits: esHits.length, indexPattern, minimumVisibleRows: 10, - onAddColumn: jest.fn(), - onAddFilter: jest.fn(), - onChangeInterval: jest.fn(), - onMoveColumn: jest.fn(), - onRemoveColumn: jest.fn(), - onSetColumns: jest.fn(), onSkipBottomButtonClick: jest.fn(), - onSort: jest.fn(), opts: { config: mockUiSettings, data: dataPluginMock.createStartContract(), @@ -74,20 +75,18 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { navigateTo: jest.fn(), sampleSize: 10, savedSearch: savedSearchMock, - setAppState: jest.fn(), setHeaderActionMenu: jest.fn(), - stateContainer: state, timefield: indexPattern.timeFieldName || '', + setAppState: jest.fn(), + services, + stateContainer: {} as GetStateReturn, }, resetQuery: jest.fn(), resultState: 'ready', rows: esHits, searchSource: searchSourceMock, - setIndexPattern: jest.fn(), state: { columns: [] }, - timefilterUpdateHandler: jest.fn(), updateQuery: jest.fn(), - updateSavedQueryId: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index baee0623f0b5a..99baa30e18c7a 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -5,9 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import './discover.scss'; -import React, { useState, useRef, useMemo } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { EuiButtonEmpty, EuiButtonIcon, @@ -21,37 +20,34 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import classNames from 'classnames'; import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; -import { getServices } from '../../kibana_services'; import { DiscoverHistogram, DiscoverUninitialized } from '../angular/directives'; import { DiscoverNoResults } from './no_results'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { DocTableLegacy, DocTableLegacyProps } from '../angular/doc_table/create_doc_table_react'; +import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; import { SkipBottomButton } from './skip_bottom_button'; -import { search } from '../../../../data/public'; -import { - DiscoverSidebarResponsive, - DiscoverSidebarResponsiveProps, -} from './sidebar/discover_sidebar_responsive'; +import { esFilters, IndexPatternField, search } from '../../../../data/public'; +import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; import { getDisplayedColumns } from '../helpers/columns'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; -import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { popularizeField } from '../helpers/popularize_field'; +import { getStateColumnActions } from '../angular/doc_table/actions/columns'; +import { DocViewFilterFn } from '../doc_views/doc_views_types'; +import { DiscoverGrid } from './discover_grid/discover_grid'; +import { DiscoverTopNav } from './discover_topnav'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; -import { getTopNavLinks } from './top_nav/get_top_nav_links'; -const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => ( - -)); -const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( - -)); - -const DataGridMemoized = React.memo((props: DiscoverGridProps) => ); +const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const SidebarMemoized = React.memo(DiscoverSidebarResponsive); +const DataGridMemoized = React.memo(DiscoverGrid); +const TopNavMemoized = React.memo(DiscoverTopNav); export function Discover({ fetch, @@ -62,25 +58,15 @@ export function Discover({ hits, indexPattern, minimumVisibleRows, - onAddColumn, - onAddFilter, - onChangeInterval, - onMoveColumn, - onRemoveColumn, - onSetColumns, onSkipBottomButtonClick, - onSort, opts, resetQuery, resultState, rows, searchSource, - setIndexPattern, state, - timefilterUpdateHandler, timeRange, updateQuery, - updateSavedQueryId, unmappedFieldsConfig, }: DiscoverProps) { const [expandedDoc, setExpandedDoc] = useState(undefined); @@ -92,28 +78,9 @@ export function Discover({ }; const [toggleOn, toggleChart] = useState(true); + const { savedSearch, indexPatternList, config, services, data, setAppState } = opts; + const { trackUiMetric, capabilities, indexPatterns } = services; const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const services = useMemo(() => getServices(), []); - const topNavMenu = useMemo( - () => - getTopNavLinks({ - getFieldCounts: opts.getFieldCounts, - indexPattern, - inspectorAdapters: opts.inspectorAdapters, - navigateTo: opts.navigateTo, - savedSearch: opts.savedSearch, - services, - state: opts.stateContainer, - onOpenInspector: () => { - // prevent overlapping - setExpandedDoc(undefined); - }, - }), - [indexPattern, opts, services] - ); - const { TopNavMenu } = services.navigation.ui; - const { trackUiMetric } = services; - const { savedSearch, indexPatternList, config } = opts; const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; const bucketInterval = bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) @@ -123,6 +90,95 @@ export function Discover({ const isLegacy = services.uiSettings.get('doc_table:legacy'); const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo( + () => + getStateColumnActions({ + capabilities, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, + }), + [capabilities, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi] + ); + + const onOpenInspector = useCallback(() => { + // prevent overlapping + setExpandedDoc(undefined); + }, [setExpandedDoc]); + + const onSort = useCallback( + (sort: string[][]) => { + setAppState({ sort }); + }, + [setAppState] + ); + + const onAddFilter = useCallback( + (field: IndexPatternField | string, values: string, operation: '+' | '-') => { + const fieldName = typeof field === 'string' ? field : field.name; + popularizeField(indexPattern, fieldName, indexPatterns); + const newFilters = esFilters.generateFilters( + opts.filterManager, + field, + values, + operation, + String(indexPattern.id) + ); + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); + } + return opts.filterManager.addFilters(newFilters); + }, + [opts, indexPattern, indexPatterns, trackUiMetric] + ); + + const onChangeInterval = useCallback( + (interval: string) => { + if (interval) { + setAppState({ interval }); + } + }, + [setAppState] + ); + + const timefilterUpdateHandler = useCallback( + (ranges: { from: number; to: number }) => { + data.query.timefilter.timefilter.setTime({ + from: moment(ranges.from).toISOString(), + to: moment(ranges.to).toISOString(), + mode: 'absolute', + }); + }, + [data] + ); + + const onBackToTop = useCallback(() => { + if (scrollableDesktop && scrollableDesktop.current) { + scrollableDesktop.current.focus(); + } + // Only the desktop one needs to target a specific container + if (!isMobile() && scrollableDesktop.current) { + scrollableDesktop.current.scrollTo(0, 0); + } else if (window) { + window.scrollTo(0, 0); + } + }, [scrollableDesktop]); + + const onResize = useCallback( + (colSettings: { columnId: string; width: number }) => { + const grid = { ...state.grid } || {}; + const newColumns = { ...grid.columns } || {}; + newColumns[colSettings.columnId] = { + width: colSettings.width, + }; + const newGrid = { ...grid, columns: newColumns }; + opts.setAppState({ grid: newGrid }); + }, + [opts, state] + ); + const columns = useMemo(() => { if (!state.columns) { return []; @@ -132,20 +188,12 @@ export function Discover({ return ( -

@@ -154,16 +202,19 @@ export function Discover({ { - if (scrollableDesktop && scrollableDesktop.current) { - scrollableDesktop.current.focus(); - } - // Only the desktop one needs to target a specific container - if (!isMobile() && scrollableDesktop.current) { - scrollableDesktop.current.scrollTo(0, 0); - } else if (window) { - window.scrollTo(0, 0); - } - }} + onBackToTop={onBackToTop} onFilter={onAddFilter} onMoveColumn={onMoveColumn} onRemoveColumn={onRemoveColumn} @@ -352,19 +393,11 @@ export function Discover({ services={services} settings={state.grid} onAddColumn={onAddColumn} - onFilter={onAddFilter} + onFilter={onAddFilter as DocViewFilterFn} onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} onSort={onSort} - onResize={(colSettings: { columnId: string; width: number }) => { - const grid = { ...state.grid } || {}; - const newColumns = { ...grid.columns } || {}; - newColumns[colSettings.columnId] = { - width: colSettings.width, - }; - const newGrid = { ...grid, columns: newColumns }; - opts.setAppState({ grid: newGrid }); - }} + onResize={onResize} />

)} diff --git a/src/plugins/discover/public/application/components/discover_topnav.test.tsx b/src/plugins/discover/public/application/components/discover_topnav.test.tsx new file mode 100644 index 0000000000000..3f12386281059 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_topnav.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { DiscoverServices } from '../../build_services'; +import { AppState, GetStateReturn } from '../angular/discover_state'; +import { savedSearchMock } from '../../__mocks__/saved_search'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock'; +import { uiSettingsMock as mockUiSettings } from '../../__mocks__/ui_settings'; +import { IndexPatternAttributes } from '../../../../data/common/index_patterns'; +import { SavedObject } from '../../../../../core/types'; +import { DiscoverTopNav, DiscoverTopNavProps } from './discover_topnav'; +import { RequestAdapter } from '../../../../inspector/common/adapters/request'; +import { TopNavMenu } from '../../../../navigation/public'; + +function getProps(): DiscoverTopNavProps { + const state = ({} as unknown) as AppState; + const services = ({ + navigation: { + ui: { TopNavMenu }, + }, + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + } as unknown) as DiscoverServices; + const indexPattern = indexPatternMock; + return { + indexPattern: indexPatternMock, + opts: { + config: mockUiSettings, + data: dataPluginMock.createStartContract(), + filterManager: createFilterManagerMock(), + getFieldCounts: jest.fn(), + indexPatternList: (indexPattern as unknown) as Array>, + inspectorAdapters: { requests: {} as RequestAdapter }, + navigateTo: jest.fn(), + sampleSize: 10, + savedSearch: savedSearchMock, + services, + setAppState: jest.fn(), + setHeaderActionMenu: jest.fn(), + stateContainer: {} as GetStateReturn, + timefield: indexPattern.timeFieldName || '', + }, + state, + updateQuery: jest.fn(), + onOpenInspector: jest.fn(), + }; +} + +describe('Discover topnav component', () => { + test('setHeaderActionMenu was called', () => { + const props = getProps(); + mountWithIntl(); + expect(props.opts.setHeaderActionMenu).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_topnav.tsx b/src/plugins/discover/public/application/components/discover_topnav.tsx new file mode 100644 index 0000000000000..69a1433b6505c --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_topnav.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useMemo } from 'react'; +import { DiscoverProps } from './types'; +import { getTopNavLinks } from './top_nav/get_top_nav_links'; + +export type DiscoverTopNavProps = Pick< + DiscoverProps, + 'indexPattern' | 'updateQuery' | 'state' | 'opts' +> & { onOpenInspector: () => void }; + +export const DiscoverTopNav = ({ + indexPattern, + opts, + onOpenInspector, + state, + updateQuery, +}: DiscoverTopNavProps) => { + const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]); + const { TopNavMenu } = opts.services.navigation.ui; + const topNavMenu = useMemo( + () => + getTopNavLinks({ + getFieldCounts: opts.getFieldCounts, + indexPattern, + inspectorAdapters: opts.inspectorAdapters, + navigateTo: opts.navigateTo, + savedSearch: opts.savedSearch, + services: opts.services, + state: opts.stateContainer, + onOpenInspector, + }), + [indexPattern, opts, onOpenInspector] + ); + + const updateSavedQueryId = (newSavedQueryId: string | undefined) => { + const { appStateContainer, setAppState } = opts.stateContainer; + if (newSavedQueryId) { + setAppState({ savedQuery: newSavedQueryId }); + } else { + // remove savedQueryId from state + const newState = { + ...appStateContainer.getState(), + }; + delete newState.savedQuery; + appStateContainer.set(newState); + } + }; + return ( + + ); +}; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx index 7178eccfec4b6..73de3b14f88f6 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx @@ -7,39 +7,44 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { shallowWithIntl as shallow } from '@kbn/test/jest'; - -// @ts-ignore import { ShallowWrapper } from 'enzyme'; import { ChangeIndexPattern } from './change_indexpattern'; import { SavedObject } from 'kibana/server'; -import { DiscoverIndexPattern } from './discover_index_pattern'; +import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern'; import { EuiSelectable } from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; const indexPattern = { - id: 'test1', + id: 'the-index-pattern-id-first', title: 'test1 title', -} as IIndexPattern; +} as IndexPattern; const indexPattern1 = { - id: 'test1', + id: 'the-index-pattern-id-first', attributes: { title: 'test1 title', }, } as SavedObject; const indexPattern2 = { - id: 'test2', + id: 'the-index-pattern-id', attributes: { title: 'test2 title', }, } as SavedObject; const defaultProps = { + config: configMock, indexPatternList: [indexPattern1, indexPattern2], selectedIndexPattern: indexPattern, - setIndexPattern: jest.fn(async () => {}), + state: {}, + setAppState: jest.fn(), + useNewFieldsApi: true, + indexPatterns: indexPatternsMock, }; function getIndexPatternPickerList(instance: ShallowWrapper) { @@ -63,11 +68,11 @@ function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: describe('DiscoverIndexPattern', () => { test('Invalid props dont cause an exception', () => { - const props = { + const props = ({ indexPatternList: null, selectedIndexPattern: null, setIndexPattern: jest.fn(), - } as any; + } as unknown) as DiscoverIndexPatternProps; expect(shallow()).toMatchSnapshot(`""`); }); @@ -80,10 +85,15 @@ describe('DiscoverIndexPattern', () => { ]); }); - test('should switch data panel to target index pattern', () => { + test('should switch data panel to target index pattern', async () => { const instance = shallow(); - - selectIndexPatternPickerOption(instance, 'test2 title'); - expect(defaultProps.setIndexPattern).toHaveBeenCalledWith('test2'); + await act(async () => { + selectIndexPatternPickerOption(instance, 'test2 title'); + }); + expect(defaultProps.setAppState).toHaveBeenCalledWith({ + index: 'the-index-pattern-id', + columns: [], + sort: [], + }); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx index 29c62d5c60775..ea3e35f607be4 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx @@ -6,35 +6,63 @@ * Side Public License, v 1. */ -import React, { useState, useEffect } from 'react'; -import { SavedObject } from 'kibana/public'; -import { IIndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; +import React, { useState, useEffect, useCallback } from 'react'; +import { IUiSettingsClient, SavedObject } from 'kibana/public'; +import { + IndexPattern, + IndexPatternAttributes, + IndexPatternsContract, +} from 'src/plugins/data/public'; import { I18nProvider } from '@kbn/i18n/react'; import { IndexPatternRef } from './types'; import { ChangeIndexPattern } from './change_indexpattern'; +import { getSwitchIndexPatternAppState } from '../../helpers/get_switch_index_pattern_app_state'; +import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; +import { MODIFY_COLUMNS_ON_SWITCH } from '../../../../common'; +import { AppState } from '../../angular/discover_state'; export interface DiscoverIndexPatternProps { + /** + * Client of uiSettings + */ + config: IUiSettingsClient; /** * list of available index patterns, if length > 1, component offers a "change" link */ indexPatternList: Array>; + /** + * Index patterns service + */ + indexPatterns: IndexPatternsContract; /** * currently selected index pattern, due to angular issues it's undefined at first rendering */ - selectedIndexPattern: IIndexPattern; + selectedIndexPattern: IndexPattern; + /** + * Function to set the current state + */ + setAppState: (state: Partial) => void; + /** + * Discover App state + */ + state: AppState; /** - * triggered when user selects a new index pattern + * Read from the Fields API */ - setIndexPattern: (id: string) => void; + useNewFieldsApi?: boolean; } /** * Component allows you to select an index pattern in discovers side bar */ export function DiscoverIndexPattern({ + config, indexPatternList, selectedIndexPattern, - setIndexPattern, + indexPatterns, + state, + setAppState, + useNewFieldsApi, }: DiscoverIndexPatternProps) { const options: IndexPatternRef[] = (indexPatternList || []).map((entity) => ({ id: entity.id, @@ -42,6 +70,24 @@ export function DiscoverIndexPattern({ })); const { id: selectedId, title: selectedTitle } = selectedIndexPattern || {}; + const setIndexPattern = useCallback( + async (id: string) => { + const nextIndexPattern = await indexPatterns.get(id); + if (nextIndexPattern && selectedIndexPattern) { + const nextAppState = getSwitchIndexPatternAppState( + selectedIndexPattern, + nextIndexPattern, + state.columns || [], + (state.sort || []) as SortPairArr[], + config.get(MODIFY_COLUMNS_ON_SWITCH), + useNewFieldsApi + ); + setAppState(nextAppState); + } + }, + [selectedIndexPattern, state, config, indexPatterns, setAppState, useNewFieldsApi] + ); + const [selected, setSelected] = useState({ id: selectedId, title: selectedTitle || '', diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 9c33bbcbc200a..0ff70585af144 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -24,6 +24,8 @@ import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; const mockServices = ({ history: () => ({ @@ -56,7 +58,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), })); -function getCompProps() { +function getCompProps(): DiscoverSidebarProps { const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -84,20 +86,22 @@ function getCompProps() { } } return { + config: configMock, columns: ['extension'], fieldCounts, hits, indexPatternList, + indexPatterns: indexPatternsMock, onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, services: mockServices, - setIndexPattern: jest.fn(), state: {}, trackUiMetric: jest.fn(), fieldFilter: getDefaultFieldFilter(), setFieldFilter: jest.fn(), + setAppState: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index db5f40d8e13cb..f0303553dfac0 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -9,7 +9,6 @@ import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { UiCounterMetricType } from '@kbn/analytics'; import { EuiAccordion, EuiFlexItem, @@ -25,122 +24,42 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; -import { IndexPatternAttributes } from '../../../../../data/common'; -import { SavedObject } from '../../../../../../core/types'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; -import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { IndexPatternField } from '../../../../../data/public'; import { getDetails } from './lib/get_details'; import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; -import { DiscoverServices } from '../../../build_services'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; -export interface DiscoverSidebarProps { - /** - * Determines whether add/remove buttons are displayed not only when focused - */ - alwaysShowActionButtons?: boolean; - /** - * the selected columns displayed in the doc table in discover - */ - columns: string[]; - /** - * a statistics of the distribution of fields in the given hits - */ - fieldCounts: Record; +export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps { /** * Current state of the field filter, filtering fields by name, type, ... */ fieldFilter: FieldFilterState; - /** - * hits fetched from ES, displayed in the doc table - */ - hits: ElasticSearchHit[]; - /** - * List of available index patterns - */ - indexPatternList: Array>; - /** - * Callback function when selecting a field - */ - onAddField: (fieldName: string) => void; - /** - * Callback function when adding a filter from sidebar - */ - onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; - /** - * Callback function when removing a field - * @param fieldName - */ - onRemoveField: (fieldName: string) => void; - /** - * Currently selected index pattern - */ - selectedIndexPattern?: IndexPattern; - /** - * Discover plugin services; - */ - services: DiscoverServices; /** * Change current state of fieldFilter */ setFieldFilter: (next: FieldFilterState) => void; - /** - * Callback function to select another index pattern - */ - setIndexPattern: (id: string) => void; - /** - * If on, fields are read from the fields API, not from source - */ - useNewFieldsApi?: boolean; - /** - * Metric tracking function - * @param metricType - * @param eventName - */ - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; - /** - * Shows index pattern and a button that displays the sidebar in a flyout - */ - useFlyout?: boolean; - - /** - * an object containing properties for proper handling of unmapped fields in the UI - */ - unmappedFieldsConfig?: { - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; - /** - * determines whether to display unmapped fields - * configurable through the switch in the UI - */ - showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; - }; } export function DiscoverSidebar({ alwaysShowActionButtons = false, columns, + config, fieldCounts, fieldFilter, hits, indexPatternList, + indexPatterns, onAddField, onAddFilter, onRemoveField, selectedIndexPattern, services, + setAppState, setFieldFilter, - setIndexPattern, + state, trackUiMetric, useNewFieldsApi = false, useFlyout = false, @@ -240,9 +159,13 @@ export function DiscoverSidebar({ })} > o.attributes.title)} + indexPatterns={indexPatterns} + state={state} + setAppState={setAppState} + useNewFieldsApi={useNewFieldsApi} /> ); @@ -266,9 +189,13 @@ export function DiscoverSidebar({ > o.attributes.title)} + indexPatterns={indexPatterns} + state={state} + setAppState={setAppState} + useNewFieldsApi={useNewFieldsApi} /> diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index 7ee6cb56d99f2..02ab5abade7fb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -15,15 +15,19 @@ import realHits from 'fixtures/real_hits.js'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternAttributes } from '../../../../../data/common'; import { getStubIndexPattern } from '../../../../../data/public/test_utils'; import { SavedObject } from '../../../../../../core/types'; -import { FieldFilterState } from './lib/field_filter'; -import { DiscoverSidebarResponsive } from './discover_sidebar_responsive'; +import { + DiscoverSidebarResponsive, + DiscoverSidebarResponsiveProps, +} from './discover_sidebar_responsive'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { DiscoverSidebar } from './discover_sidebar'; const mockServices = ({ history: () => ({ @@ -56,7 +60,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), })); -function getCompProps() { +function getCompProps(): DiscoverSidebarResponsiveProps { const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -85,25 +89,25 @@ function getCompProps() { } return { columns: ['extension'], + config: configMock, fieldCounts, hits, indexPatternList, + indexPatterns: indexPatternsMock, onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, services: mockServices, - setIndexPattern: jest.fn(), + setAppState: jest.fn(), state: {}, trackUiMetric: jest.fn(), - fieldFilter: {} as FieldFilterState, - setFieldFilter: jest.fn(), }; } describe('discover responsive sidebar', function () { - let props: DiscoverSidebarProps; - let comp: ReactWrapper; + let props: DiscoverSidebarResponsiveProps; + let comp: ReactWrapper; beforeAll(() => { props = getCompProps(); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx index b8e8fd0679baa..b689db1296922 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -11,6 +11,7 @@ import { sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { UiCounterMetricType } from '@kbn/analytics'; +import { IUiSettingsClient } from 'kibana/public'; import { EuiTitle, EuiHideFor, @@ -25,13 +26,14 @@ import { EuiPortal, } from '@elastic/eui'; import { DiscoverIndexPattern } from './discover_index_pattern'; -import { IndexPatternAttributes } from '../../../../../data/common'; +import { IndexPatternAttributes, IndexPatternsContract } from '../../../../../data/common'; import { SavedObject } from '../../../../../../core/types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { AppState } from '../../angular/discover_state'; export interface DiscoverSidebarResponsiveProps { /** @@ -42,6 +44,10 @@ export interface DiscoverSidebarResponsiveProps { * the selected columns displayed in the doc table in discover */ columns: string[]; + /** + * Client of uiSettings + */ + config: IUiSettingsClient; /** * a statistics of the distribution of fields in the given hits */ @@ -54,6 +60,10 @@ export interface DiscoverSidebarResponsiveProps { * List of available index patterns */ indexPatternList: Array>; + /** + * Index patterns service + */ + indexPatterns: IndexPatternsContract; /** * Has been toggled closed */ @@ -80,9 +90,13 @@ export interface DiscoverSidebarResponsiveProps { */ services: DiscoverServices; /** - * Callback function to select another index pattern + * Function to set the current state + */ + setAppState: (state: Partial) => void; + /** + * Discover App state */ - setIndexPattern: (id: string) => void; + state: AppState; /** * Metric tracking function * @param metricType @@ -151,9 +165,13 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) )} > o.attributes.title)} + indexPatterns={props.indexPatterns} + state={props.state} + setAppState={props.setAppState} + useNewFieldsApi={props.useNewFieldsApi} /> diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index b73f7391bf22a..ee06bcab6528b 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -9,7 +9,7 @@ import { IUiSettingsClient, MountPoint, SavedObject } from 'kibana/public'; import { Chart } from '../angular/helpers/point_series'; import { IndexPattern } from '../../../../data/common/index_patterns/index_patterns'; -import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types'; +import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { AggConfigs } from '../../../../data/common/search/aggs'; import { @@ -23,6 +23,7 @@ import { import { SavedSearch } from '../../saved_searches'; import { AppState, GetStateReturn } from '../angular/discover_state'; import { RequestAdapter } from '../../../../inspector/common'; +import { DiscoverServices } from '../../build_services'; export interface DiscoverProps { /** @@ -59,38 +60,10 @@ export interface DiscoverProps { * Increased when scrolling down */ minimumVisibleRows: number; - /** - * Function to add a column to state - */ - onAddColumn: (column: string) => void; - /** - * Function to add a filter to state - */ - onAddFilter: DocViewFilterFn; - /** - * Function to change the used time interval of the date histogram - */ - onChangeInterval: (interval: string) => void; - /** - * Function to move a given column to a given index, used in legacy table - */ - onMoveColumn: (columns: string, newIdx: number) => void; - /** - * Function to remove a given column from state - */ - onRemoveColumn: (column: string) => void; - /** - * Function to replace columns in state - */ - onSetColumns: (columns: string[]) => void; /** * Function to scroll down the legacy table to the bottom */ onSkipBottomButtonClick: () => void; - /** - * Function to change sorting of the table, triggers a fetch - */ - onSort: (sort: string[][]) => void; opts: { /** * Date histogram aggregation config @@ -108,10 +81,6 @@ export interface DiscoverProps { * Use angular router for navigation */ navigateTo: () => void; - /** - * Functions to get/mutate state - */ - stateContainer: GetStateReturn; /** * Inspect, for analyzing requests and responses */ @@ -128,6 +97,10 @@ export interface DiscoverProps { * List of available index patterns */ indexPatternList: Array>; + /** + * Kibana core services used by discover + */ + services: DiscoverServices; /** * The number of documents that can be displayed in the table/grid */ @@ -140,6 +113,10 @@ export interface DiscoverProps { * Function to set the header menu */ setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + /** + * Functions for retrieving/mutating state + */ + stateContainer: GetStateReturn; /** * Timefield of the currently used index pattern */ @@ -165,18 +142,10 @@ export interface DiscoverProps { * Instance of SearchSource, the high level search API */ searchSource: ISearchSource; - /** - * Function to change the current index pattern - */ - setIndexPattern: (id: string) => void; /** * Current app state of URL */ state: AppState; - /** - * Function to update the time filter - */ - timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; /** * Currently selected time range */ @@ -185,10 +154,6 @@ export interface DiscoverProps { * Function to update the actual query */ updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; - /** - * Function to update the actual savedQuery id - */ - updateSavedQueryId: (savedQueryId?: string) => void; /** * An object containing properties for proper handling of unmapped fields in the UI */ diff --git a/src/plugins/discover/public/application/helpers/popularize_field.ts b/src/plugins/discover/public/application/helpers/popularize_field.ts index b97b6f46600ae..4ade7d1768419 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.ts +++ b/src/plugins/discover/public/application/helpers/popularize_field.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { IndexPattern, IndexPatternsService } from '../../../../data/public'; +import { IndexPattern, IndexPatternsContract } from '../../../../data/public'; async function popularizeField( indexPattern: IndexPattern, fieldName: string, - indexPatternsService: IndexPatternsService + indexPatternsService: IndexPatternsContract ) { if (!indexPattern.id) return; const field = indexPattern.fields.getByName(fieldName); From ea96eeccb45d4fd944a62ab8a6a09e0b9f6e5b52 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 8 Feb 2021 17:44:35 +0300 Subject: [PATCH 007/100] [VEGA] src/plugins/vis_type_vega/public/lib/vega.js should be removed (#89861) * [VEGA] src/plugins/vis_type_vega/public/lib/vega.js should be removed * remove leaflet dependency * fix CI * cleanup vega-scenagraph Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- src/plugins/maps_legacy/public/leaflet.js | 1 - .../vis_type_vega/public/data_model/types.ts | 35 +++++++------ .../public/data_model/vega_parser.test.js | 5 -- .../public/data_model/vega_parser.ts | 13 ++--- src/plugins/vis_type_vega/public/lib/vega.js | 12 ----- src/plugins/vis_type_vega/public/vega_fn.ts | 2 +- src/plugins/vis_type_vega/public/vega_type.ts | 3 +- .../public/vega_view/vega_base_view.js | 29 +++++------ .../vega_view/vega_map_view/constants.ts | 3 +- .../vega_map_view/layers/vega_layer.test.ts | 5 +- .../vega_map_view/layers/vega_layer.ts | 8 ++- .../vega_map_view/utils/vsi_helper.ts | 5 +- .../vega_view/vega_map_view/view.test.ts | 12 ++--- .../public/vega_view/vega_map_view/view.ts | 14 +++--- .../public/vega_view/vega_view.js | 4 +- .../public/vega_visualization.test.js | 5 -- src/plugins/vis_type_vega/server/types.ts | 3 +- .../register_vega_collector.test.ts | 3 +- src/plugins/vis_type_vega/tsconfig.json | 3 +- yarn.lock | 50 ++----------------- 21 files changed, 75 insertions(+), 142 deletions(-) delete mode 100644 src/plugins/vis_type_vega/public/lib/vega.js diff --git a/package.json b/package.json index a5c6fa6f7b3c2..b224f0c1ae0d5 100644 --- a/package.json +++ b/package.json @@ -714,7 +714,6 @@ "leaflet": "1.5.1", "leaflet-draw": "0.4.14", "leaflet-responsive-popup": "0.6.4", - "leaflet-vega": "^0.8.6", "leaflet.heat": "0.2.0", "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", @@ -833,6 +832,7 @@ "val-loader": "^1.1.1", "vega": "^5.19.1", "vega-lite": "^4.17.0", + "vega-spec-injector": "^0.0.2", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", diff --git a/src/plugins/maps_legacy/public/leaflet.js b/src/plugins/maps_legacy/public/leaflet.js index 69531013abae4..fd02f83d72823 100644 --- a/src/plugins/maps_legacy/public/leaflet.js +++ b/src/plugins/maps_legacy/public/leaflet.js @@ -12,7 +12,6 @@ if (!window.hasOwnProperty('L')) { window.L.Browser.touch = false; window.L.Browser.pointer = false; - require('leaflet-vega'); require('leaflet.heat/dist/leaflet-heat.js'); require('leaflet-draw/dist/leaflet.draw.css'); require('leaflet-draw/dist/leaflet.draw.js'); diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index 8d6a8227203d2..042ffac583e98 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -10,6 +10,8 @@ import { SearchResponse, SearchParams } from 'elasticsearch'; import { Filter } from 'src/plugins/data/public'; import { DslQuery } from 'src/plugins/data/common'; +import { Assign } from '@kbn/utility-types'; +import { Spec } from 'vega'; import { EsQueryParser } from './es_query_parser'; import { EmsFileParser } from './ems_file_parser'; import { UrlParser } from './url_parser'; @@ -93,21 +95,24 @@ export interface KibanaConfig { renderer: Renderer; } -export interface VegaSpec { - [index: string]: any; - $schema: string; - data?: Data; - encoding?: Encoding; - mark?: string; - title?: string; - autosize?: AutoSize; - projections?: Projection[]; - width?: number | 'container'; - height?: number | 'container'; - padding?: number | Padding; - _hostConfig?: KibanaConfig; - config: VegaSpecConfig; -} +export type VegaSpec = Assign< + Spec, + { + [index: string]: any; + $schema: string; + data?: Data; + encoding?: Encoding; + mark?: string; + title?: string; + autosize?: AutoSize; + projections?: Projection[]; + width?: number | 'container'; + height?: number | 'container'; + padding?: number | Padding; + _hostConfig?: KibanaConfig; + config: VegaSpecConfig; + } +>; export enum CONSTANTS { TIMEFILTER = '%timefilter%', diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index eeacec0834ea6..1948792d55a83 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -13,11 +13,6 @@ import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; jest.mock('../services'); -jest.mock('../lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); - describe(`VegaParser.parseAsync`, () => { test(`should throw an error in case of $spec is not defined`, async () => { const vp = new VegaParser('{}'); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 667350b693a54..e97418581a42f 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -13,8 +13,9 @@ import hjson from 'hjson'; import { euiPaletteColorBlind } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { vega, vegaLite } from '../lib/vega'; + +import { logger, Warn, version as vegaVersion } from 'vega'; +import { compile, TopLevelSpec, version as vegaLiteVersion } from 'vega-lite'; import { EsQueryParser } from './es_query_parser'; import { Utils } from './utils'; import { EmsFileParser } from './ems_file_parser'; @@ -235,9 +236,9 @@ The URL is an identifier only. Kibana and your browser will never access this UR */ private _compileVegaLite() { this.vlspec = this.spec; - const logger = vega.logger(vega.Warn); // note: eslint has a false positive here - logger.warn = this._onWarning.bind(this); - this.spec = vegaLite.compile(this.vlspec, logger).spec; + const vegaLogger = logger(Warn); // note: eslint has a false positive here + vegaLogger.warn = this._onWarning.bind(this); + this.spec = compile(this.vlspec as TopLevelSpec, { logger: vegaLogger }).spec; // When using VL with the type=map and user did not provid their own projection settings, // remove the default projection that was generated by VegaLite compiler. @@ -534,7 +535,7 @@ The URL is an identifier only. Kibana and your browser will never access this UR private parseSchema(spec: VegaSpec) { const schema = schemaParser(spec.$schema); const isVegaLite = schema.library === 'vega-lite'; - const libVersion = isVegaLite ? vegaLite.version : vega.version; + const libVersion = isVegaLite ? vegaLiteVersion : vegaVersion; if (versionCompare(schema.version, libVersion) > 0) { this._onWarning( diff --git a/src/plugins/vis_type_vega/public/lib/vega.js b/src/plugins/vis_type_vega/public/lib/vega.js deleted file mode 100644 index b7c59fce6dec2..0000000000000 --- a/src/plugins/vis_type_vega/public/lib/vega.js +++ /dev/null @@ -1,12 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as vegaLite from 'vega-lite/build-es5/vega-lite'; -import * as vega from 'vega/build-es5/vega'; - -export { vega, vegaLite }; diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index fb36a0097c970..76479cbcdf1ec 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -16,7 +16,7 @@ import { VegaInspectorAdapters } from './vega_inspector/index'; import { KibanaContext, TimeRange, Query } from '../../data/public'; import { VegaParser } from './data_model/vega_parser'; -type Input = KibanaContext | null; +type Input = KibanaContext | { type: 'null' }; type Output = Promise>; interface Arguments { diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 54d4cf16f0cde..902f79d03e680 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -19,7 +19,6 @@ import { toExpressionAst } from './to_ast'; import { getInfoMessage } from './components/experimental_map_vis_info'; import { VegaVisEditorComponent } from './components/vega_vis_editor_lazy'; -import type { VegaSpec } from './data_model/types'; import type { VisParams } from './vega_fn'; export const createVegaTypeDefinition = (): VisTypeDefinition => { @@ -58,7 +57,7 @@ export const createVegaTypeDefinition = (): VisTypeDefinition => { try { const spec = parse(visParams.spec, { legacyRoot: false, keepWsc: true }); - return extractIndexPatternsFromSpec(spec as VegaSpec); + return extractIndexPatternsFromSpec(spec); } catch (e) { // spec is invalid } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 2ef687594ce06..d9b1b536a6d17 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -9,7 +9,8 @@ import $ from 'jquery'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { vega, vegaLite } from '../lib/vega'; +import { scheme, loader, logger, Warn, version as vegaVersion, expressionFunction } from 'vega'; +import { version as vegaLiteVersion } from 'vega-lite'; import { Utils } from '../data_model/utils'; import { euiPaletteColorBlind } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -19,7 +20,7 @@ import { esFilters } from '../../../data/public'; import { getEnableExternalUrls, getData } from '../services'; import { extractIndexPatternsFromSpec } from '../lib/extract_index_pattern'; -vega.scheme('elastic', euiPaletteColorBlind()); +scheme('elastic', euiPaletteColorBlind()); // Vega's extension functions are global. When called, // we forward execution to the instance-specific handler @@ -32,8 +33,8 @@ const vegaFunctions = { }; for (const funcName of Object.keys(vegaFunctions)) { - if (!vega.expressionFunction(funcName)) { - vega.expressionFunction(funcName, function handlerFwd(...args) { + if (!expressionFunction(funcName)) { + expressionFunction(funcName, function handlerFwd(...args) { const view = this.context.dataflow; view.runAfter(() => view._kibanaView.vegaFunctionsHandler(funcName, ...args)); }); @@ -164,9 +165,9 @@ export class VegaBaseView { }; // Override URL sanitizer to prevent external data loading (if disabled) - const loader = vega.loader(); - const originalSanitize = loader.sanitize.bind(loader); - loader.sanitize = (uri, options) => { + const vegaLoader = loader(); + const originalSanitize = vegaLoader.sanitize.bind(vegaLoader); + vegaLoader.sanitize = (uri, options) => { if (uri.bypassToken === bypassToken) { // If uri has a bypass token, the uri was encoded by bypassExternalUrlCheck() above. // because user can only supply pure JSON data structure. @@ -185,14 +186,14 @@ export class VegaBaseView { } return originalSanitize(uri, options); }; - config.loader = loader; + config.loader = vegaLoader; - const logger = vega.logger(vega.Warn); + const vegaLogger = logger(Warn); - logger.warn = this.onWarn.bind(this); - logger.error = this.onError.bind(this); + vegaLogger.warn = this.onWarn.bind(this); + vegaLogger.error = this.onError.bind(this); - config.logger = logger; + config.logger = vegaLogger; return config; } @@ -430,8 +431,8 @@ export class VegaBaseView { } const debugObj = {}; window.VEGA_DEBUG = debugObj; - window.VEGA_DEBUG.VEGA_VERSION = vega.version; - window.VEGA_DEBUG.VEGA_LITE_VERSION = vegaLite.version; + window.VEGA_DEBUG.VEGA_VERSION = vegaVersion; + window.VEGA_DEBUG.VEGA_LITE_VERSION = vegaLiteVersion; window.VEGA_DEBUG.view = view; window.VEGA_DEBUG.vega_spec = spec; window.VEGA_DEBUG.vegalite_spec = vlspec; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts index f200d27e1b967..3dc245f196774 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { Style } from 'mapbox-gl'; import { TMS_IN_YML_ID } from '../../../../maps_legacy/public'; export const vegaLayerId = 'vega'; @@ -16,7 +17,7 @@ export const defaultMapConfig = { tileSize: 256, }; -export const defaultMabBoxStyle = { +export const defaultMabBoxStyle: Style = { /** * according to the MapBox documentation that value should be '8' * @see (https://docs.mapbox.com/mapbox-gl-js/style-spec/root/#version) diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts index 963c2bd03f415..da4c14c77bc98 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts @@ -7,6 +7,7 @@ */ import { initVegaLayer } from './vega_layer'; +import type { View } from 'vega'; type InitVegaLayerParams = Parameters[0]; @@ -32,9 +33,9 @@ describe('vega_map_view/tms_raster_layer', () => { addLayer: jest.fn(), } as unknown) as MapType; context = { - vegaView: { + vegaView: ({ initialize: jest.fn(), - }, + } as unknown) as View, updateVegaView: jest.fn(), }; }); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts index 884e948e2aea3..a3efba804b454 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts @@ -7,14 +7,12 @@ */ import type { Map, CustomLayerInterface } from 'mapbox-gl'; +import type { View } from 'vega'; import type { LayerParameters } from './types'; -// @ts-ignore -import { vega } from '../../lib/vega'; - export interface VegaLayerContext { - vegaView: vega.View; - updateVegaView: (map: Map, view: vega.View) => void; + vegaView: View; + updateVegaView: (map: Map, view: View) => void; } export function initVegaLayer({ diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts index 29c8d33cf3967..2085e250045f6 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts @@ -7,13 +7,12 @@ */ // @ts-expect-error -// eslint-disable-next-line import/no-extraneous-dependencies import Vsi from 'vega-spec-injector'; -import { VegaSpec } from '../../../data_model/types'; +import { Spec } from 'vega'; import { defaultProjection } from '../constants'; -export const injectMapPropsIntoSpec = (spec: VegaSpec) => { +export const injectMapPropsIntoSpec = (spec: Spec) => { const vsi = new Vsi(); vsi.overrideField(spec, 'autosize', 'none'); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index b59e1c65ab3f8..21c18e15c242c 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -28,11 +28,8 @@ import { setMapServiceSettings, setUISettings, } from '../../services'; - -jest.mock('../../lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); +import { initVegaLayer, initTmsRasterLayer } from './layers'; +import { Map, NavigationControl, Style } from 'mapbox-gl'; jest.mock('mapbox-gl', () => ({ Map: jest.fn().mockImplementation(() => ({ @@ -55,9 +52,6 @@ jest.mock('./layers', () => ({ initTmsRasterLayer: jest.fn(), })); -import { initVegaLayer, initTmsRasterLayer } from './layers'; -import { Map, NavigationControl } from 'mapbox-gl'; - describe('vega_map_view/view', () => { describe('VegaMapView', () => { const coreStart = coreMock.createStart(); @@ -76,7 +70,7 @@ describe('vega_map_view/view', () => { setUISettings(coreStart.uiSettings); const getTmsService = jest.fn().mockReturnValue(({ - getVectorStyleSheet: () => ({ + getVectorStyleSheet: (): Style => ({ version: 8, sources: {}, layers: [], diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index 1cdc3af733589..4c155d6b5ea88 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { Map, Style, NavigationControl, MapboxOptions } from 'mapbox-gl'; +import { View, parse } from 'vega'; import { initTmsRasterLayer, initVegaLayer } from './layers'; import { VegaBaseView } from '../vega_base_view'; import { getMapServiceSettings } from '../../services'; @@ -24,12 +25,9 @@ import { import { validateZoomSettings, injectMapPropsIntoSpec } from './utils'; -// @ts-expect-error -import { vega } from '../../lib/vega'; - import './vega_map_view.scss'; -async function updateVegaView(mapBoxInstance: Map, vegaView: vega.View) { +async function updateVegaView(mapBoxInstance: Map, vegaView: View) { const mapCanvas = mapBoxInstance.getCanvas(); const { lat, lng } = mapBoxInstance.getCenter(); let shouldRender = false; @@ -77,7 +75,7 @@ export class VegaMapView extends VegaBaseView { }; } - private async initMapContainer(vegaView: vega.View) { + private async initMapContainer(vegaView: View) { let style: Style = defaultMabBoxStyle; let customAttribution: MapboxOptions['customAttribution'] = []; const zoomSettings = { @@ -139,7 +137,7 @@ export class VegaMapView extends VegaBaseView { } } - private initLayers(mapBoxInstance: Map, vegaView: vega.View) { + private initLayers(mapBoxInstance: Map, vegaView: View) { const shouldShowUserConfiguredLayer = this.mapStyle === userConfiguredLayerId; if (shouldShowUserConfiguredLayer) { @@ -168,8 +166,8 @@ export class VegaMapView extends VegaBaseView { } protected async _initViewCustomizations() { - const vegaView = new vega.View( - vega.parse(injectMapPropsIntoSpec(this._parser.spec)), + const vegaView = new View( + parse(injectMapPropsIntoSpec(this._parser.spec)), this._vegaViewConfig ); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index 5d5f3ed3d3733..5b1e49a73343b 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { vega } from '../lib/vega'; +import { View, parse } from 'vega'; import { VegaBaseView } from './vega_base_view'; export class VegaView extends VegaBaseView { @@ -14,7 +14,7 @@ export class VegaView extends VegaBaseView { // In some cases, Vega may be initialized twice... TBD if (!this._$container) return; - const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig); + const view = new View(parse(this._parser.spec), this._vegaViewConfig); if (this._parser.useResize) this.updateVegaSize(view); view.initialize(this._$container.get(0), this._$controls.get(0)); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index a55d5c4423f0e..776f8898b3e3a 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -26,11 +26,6 @@ jest.mock('./default_spec', () => ({ getDefaultSpec: () => jest.requireActual('./test_utils/default.spec.json'), })); -jest.mock('./lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); - // FLAKY: https://github.com/elastic/kibana/issues/71713 describe('VegaVisualizations', () => { let domNode; diff --git a/src/plugins/vis_type_vega/server/types.ts b/src/plugins/vis_type_vega/server/types.ts index f1e97416d7665..affd93dedb8ca 100644 --- a/src/plugins/vis_type_vega/server/types.ts +++ b/src/plugins/vis_type_vega/server/types.ts @@ -7,10 +7,11 @@ */ import { Observable } from 'rxjs'; +import { SharedGlobalConfig } from 'kibana/server'; import { HomeServerPluginSetup } from '../../home/server'; import { UsageCollectionSetup } from '../../usage_collection/server'; -export type ConfigObservable = Observable<{ kibana: { index: string } }>; +export type ConfigObservable = Observable; export interface VegaSavedObjectAttributes { title: string; diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts index b3abc46070159..9db1b7657f444 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts @@ -12,11 +12,12 @@ import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/ser import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { HomeServerPluginSetup } from '../../../home/server'; import { registerVegaUsageCollector } from './register_vega_collector'; +import { ConfigObservable } from '../types'; describe('registerVegaUsageCollector', () => { const mockIndex = 'mock_index'; const mockDeps = { home: ({} as unknown) as HomeServerPluginSetup }; - const mockConfig = of({ kibana: { index: mockIndex } }); + const mockConfig = of({ kibana: { index: mockIndex } }) as ConfigObservable; it('makes a usage collector and registers it`', () => { const mockCollectorSet = createUsageCollectionSetupMock(); diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index c013056ba4566..d03ee6eae790e 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "strictNullChecks": false }, "include": [ "server/**/*", diff --git a/yarn.lock b/yarn.lock index fa7ebacb1cd70..6df258e9715b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19427,13 +19427,6 @@ leaflet-responsive-popup@0.6.4: resolved "https://registry.yarnpkg.com/leaflet-responsive-popup/-/leaflet-responsive-popup-0.6.4.tgz#b93d9368ef9f96d6dc911cf5b96d90e08601c6b3" integrity sha512-2D8G9aQA6NHkulDBPN9kqbUCkCpWQQ6dF0xFL11AuEIWIbsL4UC/ZPP5m8GYM0dpU6YTlmyyCh1Tz+cls5Q4dg== -leaflet-vega@^0.8.6: - version "0.8.6" - resolved "https://registry.yarnpkg.com/leaflet-vega/-/leaflet-vega-0.8.6.tgz#dd4090a6123cb983c2b732d53ec9e4daa53736b2" - integrity sha1-3UCQphI8uYPCtzLVPsnk2qU3NrI= - dependencies: - vega-spec-injector "^0.0.2" - leaflet.heat@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/leaflet.heat/-/leaflet.heat-0.2.0.tgz#109d8cf586f0adee41f05aff031e27a77fecc229" @@ -29574,7 +29567,7 @@ vega-event-selector@^2.0.6, vega-event-selector@~2.0.6: resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-2.0.6.tgz#6beb00e066b78371dde1a0f40cb5e0bbaecfd8bc" integrity sha512-UwCu50Sqd8kNZ1X/XgiAY+QAyQUmGFAwyDu7y0T5fs6/TPQnDo/Bo346NgSgINBEhEKOAMY1Nd/rPOk4UEm/ew== -vega-expression@^4.0.0, vega-expression@^4.0.1, vega-expression@~4.0.1: +vega-expression@^4.0.1, vega-expression@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-4.0.1.tgz#c03e4fc68a00acac49557faa4e4ed6ac8a59c5fd" integrity sha512-ZrDj0hP8NmrCpdLFf7Rd/xMUHGoSYsAOTaYp7uXZ2dkEH5x0uPy5laECMc8TiQvL8W+8IrN2HAWCMRthTSRe2Q== @@ -29608,24 +29601,7 @@ vega-format@^1.0.4, vega-format@~1.0.4: vega-time "^2.0.3" vega-util "^1.15.2" -vega-functions@^5.10.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.10.0.tgz#3d384111f13b3b0dd38a4fca656c5ae54b66e158" - integrity sha512-1l28OxUwOj8FEvRU62Oz2hiTuDECrvx1DPU1qLebBKhlgaKbcCk3XyHrn1kUzhMKpXq+SFv5VPxchZP47ASSvQ== - dependencies: - d3-array "^2.7.1" - d3-color "^2.0.0" - d3-geo "^2.0.1" - vega-dataflow "^5.7.3" - vega-expression "^4.0.1" - vega-scale "^7.1.1" - vega-scenegraph "^4.9.2" - vega-selections "^5.1.5" - vega-statistics "^1.7.9" - vega-time "^2.0.4" - vega-util "^1.16.0" - -vega-functions@^5.12.0, vega-functions@~5.12.0: +vega-functions@^5.10.0, vega-functions@^5.12.0, vega-functions@~5.12.0: version "5.12.0" resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.0.tgz#44bf08a7b20673dc8cf51d6781c8ea1399501668" integrity sha512-3hljmGs+gR7TbO/yYuvAP9P5laKISf1GKk4yRHLNdM61fWgKm8pI3f6LY2Hvq9cHQFTiJ3/5/Bx2p1SX5R4quQ== @@ -29752,19 +29728,7 @@ vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@~7.1.1: vega-time "^2.0.4" vega-util "^1.15.2" -vega-scenegraph@^4.9.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.2.tgz#83b1dbc34a9ab5595c74d547d6d95849d74451ed" - integrity sha512-epm1CxcB8AucXQlSDeFnmzy0FCj+HV2k9R6ch2lfLRln5lPLEfgJWgFcFhVf5jyheY0FSeHH52Q5zQn1vYI1Ow== - dependencies: - d3-path "^2.0.0" - d3-shape "^2.0.0" - vega-canvas "^1.2.5" - vega-loader "^4.3.3" - vega-scale "^7.1.1" - vega-util "^1.15.2" - -vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: +vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: version "4.9.3" resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.3.tgz#c4720550ea7ff5c8d9d0690f47fe2640547cfc6b" integrity sha512-lBvqLbXqrqRCTGJmSgzZC/tLR/o+TXfakbdhDzNdpgTavTaQ65S/67Gpj5hPpi77DvsfZUIY9lCEeO37aJhy0Q== @@ -29781,14 +29745,6 @@ vega-schema-url-parser@^2.1.0: resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.1.0.tgz#847f9cf9f1624f36f8a51abc1adb41ebc6673cb4" integrity sha512-JHT1PfOyVzOohj89uNunLPirs05Nf59isPT5gnwIkJph96rRgTIBJE7l7yLqndd7fLjr3P8JXHGAryRp74sCaQ== -vega-selections@^5.1.5: - version "5.1.5" - resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.1.5.tgz#c7662edf26c1cfb18623573b30590c9774348d1c" - integrity sha512-oRSsfkqYqA5xfEJqDpgnSDd+w0k6p6SGYisMD6rGXMxuPl0x0Uy6RvDr4nbEtB+dpWdoWEvgrsZVS6axyDNWvQ== - dependencies: - vega-expression "^4.0.0" - vega-util "^1.15.2" - vega-selections@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.3.0.tgz#810f2e7b7642fa836cf98b2e5dcc151093b1f6a7" From 5176aa6bc7b6b74dfc9fbe6b84d212e044fd43cd Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Mon, 8 Feb 2021 07:51:45 -0700 Subject: [PATCH 008/100] Update geo alerting docs to just cover geo containment (#90480) --- docs/user/alerting/geo-alert-types.asciidoc | 67 ++---------------- .../images/alert-types-tracking-select.png | Bin 37690 -> 30066 bytes 2 files changed, 7 insertions(+), 60 deletions(-) diff --git a/docs/user/alerting/geo-alert-types.asciidoc b/docs/user/alerting/geo-alert-types.asciidoc index f79885e3bc716..d9073ecca1145 100644 --- a/docs/user/alerting/geo-alert-types.asciidoc +++ b/docs/user/alerting/geo-alert-types.asciidoc @@ -1,19 +1,16 @@ [role="xpack"] -[[geo-alert-types]] -== Geo alert types +[[geo-alerting]] +== Geo alerting -Two additional stack alerts are available: -<> and <>. +Alerting now includes one additional stack alert: <>. As with other stack alerts, you need `all` access to the *Stack Alerts* feature -to be able to create and edit either of the geo alerts. +to be able to create and edit a geo alert. See <> for more information on configuring roles that provide access to this feature. [float] -=== Geo alert requirements - -To create either a *Tracking threshold* or a *Tracking containment* alert, the -following requirements must be present: +=== Geo alerting requirements +To create a *Tracking containment* alert, the following requirements must be present: - *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, and some form of entity identifier. An entity identifier is a `keyword` or `number` @@ -33,62 +30,12 @@ than the current time minus the amount of the interval. If data older than [float] === Creating a geo alert -Both *threshold* and *containment* alerts can be created by clicking the *Create* -button in the <>. +Click the *Create* button in the <>. Complete the <>. -Select <> to generate an alert when an entity crosses a boundary, and you desire the -ability to highlight lines of crossing on a custom map. -Select -<> if an entity should send out constant alerts -while contained within a boundary (this feature is optional) or if the alert is generally -just more focused around activity when an entity exists within a shape. [role="screenshot"] image::images/alert-types-tracking-select.png[Choosing a tracking alert type] -[NOTE] -================================================== -With recent advances in the alerting framework, most of the features -available in Tracking threshold alerts can be replicated with just -a little more work in Tracking containment alerts. The capabilities of Tracking -threshold alerts may be deprecated or folded into Tracking containment alerts -in the future. -================================================== - -[float] -[[alert-type-tracking-threshold]] -=== Tracking threshold -The Tracking threshold alert type runs an {es} query over indices, comparing the latest -entity locations with their previous locations. In the event that an entity has crossed a -boundary from the selected boundary index, an alert may be generated. - -[float] -==== Defining the conditions -Tracking threshold has a *Delayed evaluation offset* and 4 clauses that define the -condition to detect, as well as 2 Kuery bars used to provide additional filtering -context for each of the indices. - -[role="screenshot"] -image::images/alert-types-tracking-threshold-conditions.png[Five clauses define the condition to detect] - - -Delayed evaluation offset:: If a data source lags or is intermittent, you may supply -an optional value to evaluate alert conditions following a fixed delay. For instance, if data -is consistently indexed 5-10 minutes following its original timestamp, a *Delayed evaluation -offset* of `10 minutes` would ensure that alertable instances are still captured. -Index (entity):: This clause requires an *index or index pattern*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. -By:: This clause specifies the field to use in the previously provided -*index or index pattern* for tracking Entities. An entity is a `keyword` -or `number` field that consistently identifies the entity to be tracked. -When entity:: This clause specifies which crossing option to track. The values -*Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions -should trigger an alert. *Entered* alerts on entry into a boundary, *Exited* alerts on exit -from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances -or exits. -Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_shape` field* -identifying boundaries, and an optional *Human-readable boundary name* for better alerting -messages. - [float] [[alert-type-tracking-containment]] === Tracking containment diff --git a/docs/user/alerting/images/alert-types-tracking-select.png b/docs/user/alerting/images/alert-types-tracking-select.png index 445a5202ffd0c2c9b809d95e7d9c4d6d8a8723ca..44fcf1a2600b8e287594ef13b0bffd274994b88c 100644 GIT binary patch literal 30066 zcmdRW1yq#ryXIJcBCT{NAV@a|Lx>EZq=105NO$KDD$*t0Al=;!(jXw+3|#|5ckR#r z|L*SDy}S3^yLZo?vmQJ!@O|I>`hDK#dEW7VFDHrhi1ZNz0>OIs7N!V+-2DiF+&RFw z3w~2$MxPA6J+ywSW(R>_xBvZrCyEK13<7xuc?T2y;Hau#)-*916@&wqM^7eJmv|6^WaxwRWjzuhj(+auyb{yDhSF zpZI+!e2A(;G|m1tfA)#k2fXK*RDcHfwm@+IIRvst`VAKXamEtGfIu32p52E)w8d~i z1B;(y+<|uL5|KSvtDHtJpK?mZ9U)# zJnn0Pa)iPz7j757T`<=M!y?^L zGjy$IWIZ`hg?zF)N{KD>AQ>+@I@*;*Yp628Y@zh$$_kOVc*pL1W4+Dc_-{4TKvG?! z%b^eawK)7>WpmnL9vz!}3HvIHRlA|VG0ExUnF9*U)zv-p6N_ts2mU!=REjg7MTuZE z@?D&tThXO`y?3GYii-S}`DA{MtS^QoSG{s8J$ABHOcK1WZRi~^bjwjp7&=XEm5y_0 z)CZcctj^0`tQlrKdi02=q5=(jgUEinyJGk5Y0Ol??3p*tRv5To)C&FauIlPzDMIkIO8fK7z8D;eNhT&H zX_#=+OX|Bk zZvKLYr_f8!$U@6qr@8-eo4(OHx5d0var97TU#hv7goL3`AKi4_1re+EXQr;ul+p1q zC)8;0oIx)KOILqFbue<&Vz|OUQIS)l*74e>s>q{%x^s8lfGY*%hTb)>ozvCu0Nn0= ziSt>K#AmA9sTs6Ie9q^=1MkOeHSpg?x23+=R0d@2h6A<}<6}BYm5EB|kS`*#wXDHe znVAG($;sGG3(&}@sAy*Gm-O^qC`w`ckMG~Jn{OcqmjJl_f}l| ze4ucz8{KK!e5MR*EKk{DI0Fw)2u?Vt(xkFhLC&Ji9T*tgU@csuQW$UEQI{E2Ru=yL zrHq{1O>m*khx^*ud3nDG(Xlrs>KyW&rlvse@tvlps&>Hf(0~bnc*fXj zks3U_f7fyoYC2xP;9_v#0lSYU6cTtCTmM>n5;{|ii z)I1~3ef;q8@!Ox$(6kM z#tmy|NQ{e9%an=&=K}4_M{q*Xq|MGYXw{$Eb_vgMBat-^AYYo_)_yB7iJ((0_DK

T1+R=<4bOKr@3A)ZMu-l$DkJ-_}b6 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

Z=b$?CE%Gp~U{yd9HNKQ^@G22Il7oCaCoLa&z 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 Date: Mon, 8 Feb 2021 08:59:45 -0600 Subject: [PATCH 009/100] TypeScript project references for infra plugin (#90118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Felix Stürmer --- .../http_api/log_alerts/chart_preview_data.ts | 16 ++++- .../infra/common/http_api/shared/errors.ts | 41 +++++++++---- .../infra/common/inventory_models/types.ts | 2 +- .../use_metric_threshold_alert_prefill.ts | 2 +- .../public/components/document_title.tsx | 8 +-- .../public/components/eui/toolbar/toolbar.tsx | 18 ++++-- .../public/components/fixed_datepicker.tsx | 14 +++-- .../infra/public/components/toolbar_panel.ts | 17 ++++-- .../components/waffle/custom_field_panel.tsx | 4 +- .../inventory_view/components/waffle/node.tsx | 4 +- .../waffle/waffle_group_by_controls.tsx | 4 +- .../infra/public/utils/use_tracked_promise.ts | 8 +-- x-pack/plugins/infra/tsconfig.json | 36 +++++++++++ x-pack/test/tsconfig.json | 60 +++++++++---------- x-pack/tsconfig.json | 2 + x-pack/tsconfig.refs.json | 1 + 16 files changed, 163 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/infra/tsconfig.json diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts index 76533a476561b..e6baca305508e 100644 --- a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -41,7 +41,21 @@ export type GetLogAlertsChartPreviewDataSuccessResponsePayload = rt.TypeOf< typeof getLogAlertsChartPreviewDataSuccessResponsePayloadRT >; -export const getLogAlertsChartPreviewDataAlertParamsSubsetRT = rt.intersection([ +// This should not have an explicit `any` return type, but it's here because its +// inferred type includes `Comparator` which is a string enum exported from +// common/alerting/logs/log_threshold/types.ts. +// +// There's a bug that's fixed in TypeScript 4.2.0 that will allow us to remove +// the `:any` from this, so remove it when that update happens. +// +// If it's removed before then you get: +// +// x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts:44:14 - error TS4023: +// Exported variable 'getLogAlertsChartPreviewDataAlertParamsSubsetRT' has or is using name 'Comparator' +// from external module "/Users/smith/Code/kibana/x-pack/plugins/infra/common/alerting/logs/log_threshold/types" +// but cannot be named. +// +export const getLogAlertsChartPreviewDataAlertParamsSubsetRT: any = rt.intersection([ rt.type({ criteria: countCriteriaRT, timeUnit: timeUnitRT, diff --git a/x-pack/plugins/infra/common/http_api/shared/errors.ts b/x-pack/plugins/infra/common/http_api/shared/errors.ts index 5e439c31bbdc9..2b5461d71500e 100644 --- a/x-pack/plugins/infra/common/http_api/shared/errors.ts +++ b/x-pack/plugins/infra/common/http_api/shared/errors.ts @@ -7,18 +7,35 @@ import * as rt from 'io-ts'; -const createErrorRuntimeType = ( - statusCode: number, - errorCode: string, - attributes?: Attributes -) => +export const badRequestErrorRT = rt.intersection([ rt.type({ - statusCode: rt.literal(statusCode), - error: rt.literal(errorCode), + statusCode: rt.literal(400), + error: rt.literal('Bad Request'), message: rt.string, - ...(!!attributes ? { attributes } : {}), - }); + }), + rt.partial({ + attributes: rt.unknown, + }), +]); -export const badRequestErrorRT = createErrorRuntimeType(400, 'Bad Request'); -export const forbiddenErrorRT = createErrorRuntimeType(403, 'Forbidden'); -export const conflictErrorRT = createErrorRuntimeType(409, 'Conflict'); +export const forbiddenErrorRT = rt.intersection([ + rt.type({ + statusCode: rt.literal(403), + error: rt.literal('Forbidden'), + message: rt.string, + }), + rt.partial({ + attributes: rt.unknown, + }), +]); + +export const conflictErrorRT = rt.intersection([ + rt.type({ + statusCode: rt.literal(409), + error: rt.literal('Conflict'), + message: rt.string, + }), + rt.partial({ + attributes: rt.unknown, + }), +]); diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 2d3b6a7c45d07..764f41966261c 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -286,7 +286,7 @@ export const ESTopHitsAggRT = rt.type({ top_hits: rt.object, }); -interface SnapshotTermsWithAggregation { +export interface SnapshotTermsWithAggregation { terms: { field: string }; aggregations: MetricsUIAggregation; } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts index 3664be3b4903a..068c33ea2c31f 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts @@ -9,7 +9,7 @@ import { isEqual } from 'lodash'; import { useState } from 'react'; import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; -interface MetricThresholdPrefillOptions { +export interface MetricThresholdPrefillOptions { groupBy: string | string[] | undefined; filterQuery: string | undefined; metrics: MetricsExplorerMetric[]; diff --git a/x-pack/plugins/infra/public/components/document_title.tsx b/x-pack/plugins/infra/public/components/document_title.tsx index 9c3c89294f403..20e482d9df5b5 100644 --- a/x-pack/plugins/infra/public/components/document_title.tsx +++ b/x-pack/plugins/infra/public/components/document_title.tsx @@ -48,19 +48,19 @@ const wrapWithSharedState = () => { return null; } - private getTitle(title: TitleProp) { + public getTitle(title: TitleProp) { return typeof title === 'function' ? title(titles[this.state.index - 1]) : title; } - private pushTitle(title: string) { + public pushTitle(title: string) { titles[this.state.index] = title; } - private removeTitle() { + public removeTitle() { titles.pop(); } - private updateDocumentTitle() { + public updateDocumentTitle() { const title = (titles[titles.length - 1] || '') + TITLE_SUFFIX; if (title !== document.title) { document.title = title; diff --git a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx index a2dd383695983..f1a793d11166c 100644 --- a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx +++ b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx @@ -6,13 +6,19 @@ */ import { EuiPanel } from '@elastic/eui'; +import { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; - -export const Toolbar = euiStyled(EuiPanel).attrs(() => ({ - grow: false, - paddingSize: 'none', -}))` +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const Toolbar: StyledComponent = euiStyled(EuiPanel).attrs( + () => ({ + grow: false, + paddingSize: 'none', + }) +)` border-top: none; border-right: none; border-left: none; diff --git a/x-pack/plugins/infra/public/components/fixed_datepicker.tsx b/x-pack/plugins/infra/public/components/fixed_datepicker.tsx index 62093dbfe53ec..dfaf0a490225a 100644 --- a/x-pack/plugins/infra/public/components/fixed_datepicker.tsx +++ b/x-pack/plugins/infra/public/components/fixed_datepicker.tsx @@ -5,12 +5,18 @@ * 2.0. */ -import React from 'react'; - import { EuiDatePicker, EuiDatePickerProps } from '@elastic/eui'; -import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; +import React, { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../src/plugins/kibana_react/common'; -export const FixedDatePicker = euiStyled( +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const FixedDatePicker: StyledComponent< + FunctionComponent, + EuiTheme +> = euiStyled( ({ className, inputClassName, diff --git a/x-pack/plugins/infra/public/components/toolbar_panel.ts b/x-pack/plugins/infra/public/components/toolbar_panel.ts index 22352b97da0ea..d94e7faa0eabf 100644 --- a/x-pack/plugins/infra/public/components/toolbar_panel.ts +++ b/x-pack/plugins/infra/public/components/toolbar_panel.ts @@ -5,13 +5,20 @@ * 2.0. */ +import { FunctionComponent } from 'react'; import { EuiPanel } from '@elastic/eui'; -import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; +import { StyledComponent } from 'styled-components'; +import { EuiTheme, euiStyled } from '../../../../../src/plugins/kibana_react/common'; -export const ToolbarPanel = euiStyled(EuiPanel).attrs(() => ({ - grow: false, - paddingSize: 'none', -}))` +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const ToolbarPanel: StyledComponent = euiStyled(EuiPanel).attrs( + () => ({ + grow: false, + paddingSize: 'none', + }) +)` border-top: none; border-right: none; border-left: none; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx index 8932388398b6a..acc6ae7af2727 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx @@ -27,7 +27,7 @@ const initialState = { type State = Readonly; -export const CustomFieldPanel = class extends React.PureComponent { +export class CustomFieldPanel extends React.PureComponent { public static displayName = 'CustomFieldPanel'; public readonly state: State = initialState; public render() { @@ -86,4 +86,4 @@ export const CustomFieldPanel = class extends React.PureComponent private handleFieldSelection = (selectedOptions: SelectedOption[]) => { this.setState({ selectedOptions }); }; -}; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index c76ff798b1286..d6934c6846b79 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -44,7 +44,7 @@ interface Props { currentTime: number; } -export const Node = class extends React.PureComponent { +export class Node extends React.PureComponent { public readonly state: State = initialState; public render() { const { nodeType, node, options, squareSize, bounds, formatter, currentTime } = this.props; @@ -164,7 +164,7 @@ export const Node = class extends React.PureComponent { this.setState({ isPopoverOpen: false }); } }; -}; +} const NodeContainer = euiStyled.div` position: relative; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx index 5c57ef11380e5..9f350610b1366 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx @@ -39,7 +39,7 @@ const initialState = { type State = Readonly; -export const WaffleGroupByControls = class extends React.PureComponent { +export class WaffleGroupByControls extends React.PureComponent { public static displayName = 'WaffleGroupByControls'; public readonly state: State = initialState; @@ -192,7 +192,7 @@ export const WaffleGroupByControls = class extends React.PureComponent( return [promiseState, execute] as [typeof promiseState, typeof execute]; }; -interface UninitializedPromiseState { +export interface UninitializedPromiseState { state: 'uninitialized'; } -interface PendingPromiseState { +export interface PendingPromiseState { state: 'pending'; promise: Promise; } -interface ResolvedPromiseState { +export interface ResolvedPromiseState { state: 'resolved'; promise: Promise; value: ResolvedValue; } -interface RejectedPromiseState { +export interface RejectedPromiseState { state: 'rejected'; promise: Promise; value: RejectedValue; diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json new file mode 100644 index 0000000000000..a8a0e2c7119a9 --- /dev/null +++ b/x-pack/plugins/infra/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "../../typings/**/*", + "common/**/*", + "public/**/*", + "scripts/**/*", + "server/**/*", + "types/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/vis_type_timeseries/tsconfig.json" }, + { "path": "../data_enhanced/tsconfig.json" }, + { "path": "../alerts/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../license_management/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../observability/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" } + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 10943b3a2929f..0a7a30f373e07 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -9,73 +9,73 @@ "exclude": ["../typings/jest.d.ts"], "references": [ { "path": "../../src/core/tsconfig.json" }, - { "path": "../../src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "../../src/plugins/management/tsconfig.json" }, { "path": "../../src/plugins/bfetch/tsconfig.json" }, { "path": "../../src/plugins/charts/tsconfig.json" }, { "path": "../../src/plugins/console/tsconfig.json" }, { "path": "../../src/plugins/dashboard/tsconfig.json" }, - { "path": "../../src/plugins/discover/tsconfig.json" }, { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../../src/plugins/discover/tsconfig.json" }, { "path": "../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../../src/plugins/expressions/tsconfig.json" }, { "path": "../../src/plugins/home/tsconfig.json" }, + { "path": "../../src/plugins/index_pattern_management/tsconfig.json" }, { "path": "../../src/plugins/kibana_overview/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../src/plugins/legacy_export/tsconfig.json" }, + { "path": "../../src/plugins/management/tsconfig.json" }, { "path": "../../src/plugins/navigation/tsconfig.json" }, { "path": "../../src/plugins/newsfeed/tsconfig.json" }, - { "path": "../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../src/plugins/saved_objects_management/tsconfig.json" }, { "path": "../../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, + { "path": "../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../src/plugins/share/tsconfig.json" }, { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "../../src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "../../src/plugins/telemetry/tsconfig.json" }, - { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../src/plugins/ui_actions/tsconfig.json" }, { "path": "../../src/plugins/url_forwarding/tsconfig.json" }, - { "path": "../../src/plugins/index_pattern_management/tsconfig.json" }, - + { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../plugins/actions/tsconfig.json" }, { "path": "../plugins/alerts/tsconfig.json" }, + { "path": "../plugins/beats_management/tsconfig.json" }, + { "path": "../plugins/cloud/tsconfig.json" }, { "path": "../plugins/code/tsconfig.json" }, { "path": "../plugins/console_extensions/tsconfig.json" }, - { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/dashboard_mode/tsconfig.json" }, - { "path": "../plugins/enterprise_search/tsconfig.json" }, - { "path": "../plugins/global_search/tsconfig.json" }, - { "path": "../plugins/global_search_providers/tsconfig.json" }, - { "path": "../plugins/features/tsconfig.json" }, + { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, + { "path": "../plugins/enterprise_search/tsconfig.json" }, { "path": "../plugins/event_log/tsconfig.json" }, - { "path": "../plugins/licensing/tsconfig.json" }, + { "path": "../plugins/features/tsconfig.json" }, + { "path": "../plugins/global_search_bar/tsconfig.json" }, + { "path": "../plugins/global_search_providers/tsconfig.json" }, + { "path": "../plugins/global_search/tsconfig.json" }, + { "path": "../plugins/grokdebugger/tsconfig.json" }, + { "path": "../plugins/index_management/tsconfig.json" }, + { "path": "../plugins/infra/tsconfig.json" }, + { "path": "../plugins/ingest_pipelines/tsconfig.json" }, { "path": "../plugins/lens/tsconfig.json" }, + { "path": "../plugins/license_management/tsconfig.json" }, + { "path": "../plugins/licensing/tsconfig.json" }, { "path": "../plugins/ml/tsconfig.json" }, + { "path": "../plugins/observability/tsconfig.json" }, + { "path": "../plugins/painless_lab/tsconfig.json" }, + { "path": "../plugins/runtime_fields/tsconfig.json" }, + { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, + { "path": "../plugins/security/tsconfig.json" }, + { "path": "../plugins/snapshot_restore/tsconfig.json" }, + { "path": "../plugins/spaces/tsconfig.json" }, + { "path": "../plugins/stack_alerts/tsconfig.json" }, { "path": "../plugins/task_manager/tsconfig.json" }, { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "../plugins/transform/tsconfig.json" }, { "path": "../plugins/triggers_actions_ui/tsconfig.json" }, { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "../plugins/spaces/tsconfig.json" }, - { "path": "../plugins/security/tsconfig.json" }, - { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "../plugins/stack_alerts/tsconfig.json" }, - { "path": "../plugins/beats_management/tsconfig.json" }, - { "path": "../plugins/cloud/tsconfig.json" }, - { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "../plugins/global_search_bar/tsconfig.json" }, - { "path": "../plugins/observability/tsconfig.json" }, - { "path": "../plugins/ingest_pipelines/tsconfig.json" }, - { "path": "../plugins/license_management/tsconfig.json" }, - { "path": "../plugins/snapshot_restore/tsconfig.json" }, - { "path": "../plugins/grokdebugger/tsconfig.json" }, - { "path": "../plugins/painless_lab/tsconfig.json" }, { "path": "../plugins/upgrade_assistant/tsconfig.json" }, - { "path": "../plugins/watcher/tsconfig.json" }, - { "path": "../plugins/runtime_fields/tsconfig.json" }, - { "path": "../plugins/index_management/tsconfig.json" } + { "path": "../plugins/watcher/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 6fabd16752dfa..5d51c2923abd0 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -22,6 +22,7 @@ "plugins/embeddable_enhanced/**/*", "plugins/event_log/**/*", "plugins/enterprise_search/**/*", + "plugins/infra/**/*", "plugins/licensing/**/*", "plugins/lens/**/*", "plugins/maps/**/*", @@ -118,6 +119,7 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/grokdebugger/tsconfig.json" }, + { "path": "./plugins/infra/tsconfig.json" }, { "path": "./plugins/ingest_pipelines/tsconfig.json" }, { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index e35cfe4e024a2..ae88ab6486e64 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -23,6 +23,7 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/grokdebugger/tsconfig.json" }, + { "path": "./plugins/infra/tsconfig.json" }, { "path": "./plugins/ingest_pipelines/tsconfig.json" }, { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, From dccea865e47a8c70b8abbd7d7400172061d2c89e Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 Feb 2021 15:17:35 +0000 Subject: [PATCH 010/100] chore(NA): push important bazel config files under operations team code owners (#90610) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3884f975c813d..2917cc52a6c6d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -150,6 +150,12 @@ /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /vars/ @elastic/kibana-operations +/.bazelignore @elastic/kibana-operations +/.bazeliskversion @elastic/kibana-operations +/.bazelrc @elastic/kibana-operations +/.bazelrc.common @elastic/kibana-operations +/.bazelversion @elastic/kibana-operations +/WORKSPACE.bazel @elastic/kibana-operations #CC# /packages/kbn-expect/ @elastic/kibana-operations # Quality Assurance From bbda20619ea31f430570fa2b9e1f78142d44cbc5 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 8 Feb 2021 16:20:56 +0100 Subject: [PATCH 011/100] [Search Sessions] Disable "save session" due to timeout (#90294) --- x-pack/plugins/data_enhanced/public/plugin.ts | 4 + ...onnected_search_session_indicator.test.tsx | 139 ++++++++++++++++-- .../connected_search_session_indicator.tsx | 118 ++++++++++----- .../search_session_tour.tsx | 21 +-- .../search_session_indicator.stories.tsx | 6 +- .../search_session_indicator.test.tsx | 16 +- .../search_session_indicator.tsx | 68 +++++---- .../services/search_sessions.ts | 4 +- 8 files changed, 276 insertions(+), 100 deletions(-) diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index b7d7b7c0e20d1..0a116545e6e36 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -6,6 +6,7 @@ */ import React from 'react'; +import moment from 'moment'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; @@ -86,6 +87,9 @@ export class DataEnhancedPlugin application: core.application, timeFilter: plugins.data.query.timefilter.timefilter, storage: this.storage, + disableSaveAfterSessionCompletesTimeout: moment + .duration(this.config.search.sessions.notTouchedTimeout) + .asMilliseconds(), }) ) ), diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx index 79e49050941be..3437920ed7c98 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { StubBrowserStorage } from '@kbn/test/jest'; import { render, waitFor, screen, act } from '@testing-library/react'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public/'; @@ -20,6 +20,8 @@ import { } from '../../../../../../../src/plugins/data/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { TOUR_RESTORE_STEP_KEY, TOUR_TAKING_TOO_LONG_STEP_KEY } from './search_session_tour'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from 'react-intl'; const coreStart = coreMock.createStart(); const dataStart = dataPluginMock.createStartContract(); @@ -30,6 +32,12 @@ const timeFilter = dataStart.query.timefilter.timefilter as jest.Mocked refreshInterval$); timeFilter.getRefreshInterval.mockImplementation(() => refreshInterval$.getValue()); +const disableSaveAfterSessionCompletesTimeout = 5 * 60 * 1000; + +function Container({ children }: { children?: ReactNode }) { + return {children}; +} + beforeEach(() => { storage = new Storage(new StubBrowserStorage()); refreshInterval$.next({ value: 0, pause: true }); @@ -47,8 +55,13 @@ test("shouldn't show indicator in case no active search session", async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const { getByTestId, container } = render(); + const { getByTestId, container } = render( + + + + ); // make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading) await expect( @@ -69,8 +82,13 @@ test("shouldn't show indicator in case app hasn't opt-in", async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const { getByTestId, container } = render(); + const { getByTestId, container } = render( + + + + ); sessionService.isSessionStorageReady.mockImplementation(() => false); // make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading) @@ -93,8 +111,13 @@ test('should show indicator in case there is an active search session', async () application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const { getByTestId } = render(); + const { getByTestId } = render( + + + + ); await waitFor(() => getByTestId('searchSessionIndicator')); }); @@ -118,13 +141,20 @@ test('should be disabled in case uiConfig says so ', async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - render(); + render( + + + + ); await waitFor(() => screen.getByTestId('searchSessionIndicator')); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); }); test('should be disabled during auto-refresh', async () => { @@ -135,19 +165,82 @@ test('should be disabled during auto-refresh', async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - render(); + render( + + + + ); await waitFor(() => screen.getByTestId('searchSessionIndicator')); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).not.toBeDisabled(); + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); act(() => { refreshInterval$.next({ value: 0, pause: false }); }); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); +}); + +describe('Completed inactivity', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); + test('save should be disabled after completed and timeout', async () => { + const state$ = new BehaviorSubject(SearchSessionState.Loading); + + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + timeFilter, + storage, + disableSaveAfterSessionCompletesTimeout, + }); + + render( + + + + ); + + await waitFor(() => screen.getByTestId('searchSessionIndicator')); + + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + jest.advanceTimersByTime(5 * 60 * 1000); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + state$.next(SearchSessionState.Completed); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + jest.advanceTimersByTime(2.5 * 60 * 1000); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + jest.advanceTimersByTime(2.5 * 60 * 1000); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); + }); }); describe('tour steps', () => { @@ -167,8 +260,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); await waitFor(() => rendered.getByTestId('searchSessionIndicator')); @@ -199,8 +297,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); const searchSessionIndicator = await rendered.findByTestId('searchSessionIndicator'); expect(searchSessionIndicator).toBeTruthy(); @@ -225,8 +328,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); await waitFor(() => rendered.getByTestId('searchSessionIndicator')); expect(screen.getByTestId('searchSessionIndicatorPopoverContainer')).toBeInTheDocument(); @@ -242,8 +350,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); await waitFor(() => rendered.getByTestId('searchSessionIndicator')); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx index b572db7ebfd4c..3935b5bb2814b 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useRef } from 'react'; -import { debounce, distinctUntilChanged, map } from 'rxjs/operators'; -import { timer } from 'rxjs'; +import React, { useCallback, useState } from 'react'; +import { debounce, distinctUntilChanged, map, mapTo, switchMap } from 'rxjs/operators'; +import { merge, of, timer } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; import { SearchSessionIndicator, SearchSessionIndicatorRef } from '../search_session_indicator'; @@ -26,6 +26,11 @@ export interface SearchSessionIndicatorDeps { timeFilter: TimefilterContract; application: ApplicationStart; storage: IStorageWrapper; + /** + * Controls for how long we allow to save a session, + * after the last search in the session has completed + */ + disableSaveAfterSessionCompletesTimeout: number; } export const createConnectedSearchSessionIndicator = ({ @@ -33,6 +38,7 @@ export const createConnectedSearchSessionIndicator = ({ application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }: SearchSessionIndicatorDeps): React.FC => { const isAutoRefreshEnabled = () => !timeFilter.getRefreshInterval().pause; const isAutoRefreshEnabled$ = timeFilter @@ -43,60 +49,104 @@ export const createConnectedSearchSessionIndicator = ({ debounce((_state) => timer(_state === SearchSessionState.None ? 50 : 300)) // switch to None faster to quickly remove indicator when navigating away ); + const disableSaveAfterSessionCompleteTimedOut$ = sessionService.state$.pipe( + switchMap((_state) => + _state === SearchSessionState.Completed + ? merge(of(false), timer(disableSaveAfterSessionCompletesTimeout).pipe(mapTo(true))) + : of(false) + ), + distinctUntilChanged() + ); + return () => { - const ref = useRef(null); const state = useObservable(debouncedSessionServiceState$, SearchSessionState.None); const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled()); - const isDisabledByApp = sessionService.getSearchSessionIndicatorUiConfig().isDisabled(); + const isSaveDisabledByApp = sessionService.getSearchSessionIndicatorUiConfig().isDisabled(); + const disableSaveAfterSessionCompleteTimedOut = useObservable( + disableSaveAfterSessionCompleteTimedOut$, + false + ); + const [ + searchSessionIndicator, + setSearchSessionIndicator, + ] = useState(null); + const searchSessionIndicatorRef = useCallback((ref: SearchSessionIndicatorRef) => { + if (ref !== null) { + setSearchSessionIndicator(ref); + } + }, []); - let disabled = false; - let disabledReasonText: string = ''; + let saveDisabled = false; + let saveDisabledReasonText: string = ''; if (autoRefreshEnabled) { - disabled = true; - disabledReasonText = i18n.translate( + saveDisabled = true; + saveDisabledReasonText = i18n.translate( 'xpack.data.searchSessionIndicator.disabledDueToAutoRefreshMessage', { - defaultMessage: 'Search sessions are not available when auto refresh is enabled.', + defaultMessage: 'Saving search session is not available when auto refresh is enabled.', + } + ); + } + + if (disableSaveAfterSessionCompleteTimedOut) { + saveDisabled = true; + saveDisabledReasonText = i18n.translate( + 'xpack.data.searchSessionIndicator.disabledDueToTimeoutMessage', + { + defaultMessage: 'Search session results expired.', } ); } + if (isSaveDisabledByApp.disabled) { + saveDisabled = true; + saveDisabledReasonText = isSaveDisabledByApp.reasonText; + } + const { markOpenedDone, markRestoredDone } = useSearchSessionTour( storage, - ref, + searchSessionIndicator, state, - disabled + saveDisabled ); - if (isDisabledByApp.disabled) { - disabled = true; - disabledReasonText = isDisabledByApp.reasonText; - } + const onOpened = useCallback( + (openedState: SearchSessionState) => { + markOpenedDone(); + if (openedState === SearchSessionState.Restored) { + markRestoredDone(); + } + }, + [markOpenedDone, markRestoredDone] + ); + + const onContinueInBackground = useCallback(() => { + if (saveDisabled) return; + sessionService.save(); + }, [saveDisabled]); + + const onSaveResults = useCallback(() => { + if (saveDisabled) return; + sessionService.save(); + }, [saveDisabled]); + + const onCancel = useCallback(() => { + sessionService.cancel(); + }, []); if (!sessionService.isSessionStorageReady()) return null; return ( { - sessionService.save(); - }} - onSaveResults={() => { - sessionService.save(); - }} - onCancel={() => { - sessionService.cancel(); - }} - disabled={disabled} - disabledReasonText={disabledReasonText} - onOpened={(openedState) => { - markOpenedDone(); - if (openedState === SearchSessionState.Restored) { - markRestoredDone(); - } - }} + saveDisabled={saveDisabled} + saveDisabledReasonText={saveDisabledReasonText} + onContinueInBackground={onContinueInBackground} + onSaveResults={onSaveResults} + onCancel={onCancel} + onOpened={onOpened} /> ); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx index 8c04410f9953b..7987278f400ff 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { MutableRefObject, useCallback, useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; import { SearchSessionIndicatorRef } from '../search_session_indicator'; import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; @@ -16,7 +16,7 @@ export const TOUR_RESTORE_STEP_KEY = `data.searchSession.tour.restore`; export function useSearchSessionTour( storage: IStorageWrapper, - searchSessionIndicatorRef: MutableRefObject, + searchSessionIndicatorRef: SearchSessionIndicatorRef | null, state: SearchSessionState, searchSessionsDisabled: boolean ) { @@ -30,19 +30,20 @@ export function useSearchSessionTour( useEffect(() => { if (searchSessionsDisabled) return; + if (!searchSessionIndicatorRef) return; let timeoutHandle: number; if (state === SearchSessionState.Loading) { if (!safeHas(storage, TOUR_TAKING_TOO_LONG_STEP_KEY)) { timeoutHandle = window.setTimeout(() => { - safeOpen(searchSessionIndicatorRef); + searchSessionIndicatorRef.openPopover(); }, TOUR_TAKING_TOO_LONG_TIMEOUT); } } if (state === SearchSessionState.Restored) { if (!safeHas(storage, TOUR_RESTORE_STEP_KEY)) { - safeOpen(searchSessionIndicatorRef); + searchSessionIndicatorRef.openPopover(); } } @@ -79,15 +80,3 @@ function safeSet(storage: IStorageWrapper, key: string) { return true; } } - -function safeOpen(searchSessionIndicatorRef: MutableRefObject) { - if (searchSessionIndicatorRef.current) { - searchSessionIndicatorRef.current.openPopover(); - } else { - // TODO: needed for initial open when component is not rendered yet - // fix after: https://github.com/elastic/eui/issues/4460 - setTimeout(() => { - searchSessionIndicatorRef.current?.openPopover(); - }, 50); - } -} diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx index f2d5a3c52daea..62d95c1043800 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx @@ -33,9 +33,9 @@ storiesOf('components/SearchSessionIndicator', module).add('default', () => (

diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx index 59c39aecddb32..ff9e27cad1869 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx @@ -108,11 +108,21 @@ test('Canceled state', async () => { }); test('Disabled state', async () => { - render( + const { rerender } = render( + + + + ); + + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); + + rerender( - + ); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); }); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx index 9ac537829a670..eb58039ff58f7 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx @@ -31,8 +31,10 @@ export interface SearchSessionIndicatorProps { onCancel?: () => void; viewSearchSessionsLink?: string; onSaveResults?: () => void; - disabled?: boolean; - disabledReasonText?: string; + + saveDisabled?: boolean; + saveDisabledReasonText?: string; + onOpened?: (openedState: SearchSessionState) => void; } @@ -55,17 +57,22 @@ const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonPro const ContinueInBackgroundButton = ({ onContinueInBackground = () => {}, buttonProps = {}, + saveDisabled = false, + saveDisabledReasonText, }: ActionButtonProps) => ( - - - + + + + + ); const ViewAllSearchSessionsButton = ({ @@ -84,17 +91,25 @@ const ViewAllSearchSessionsButton = ({ ); -const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => ( - - - +const SaveButton = ({ + onSaveResults = () => {}, + buttonProps = {}, + saveDisabled = false, + saveDisabledReasonText, +}: ActionButtonProps) => ( + + + + + ); const searchSessionIndicatorViewStateToProps: { @@ -325,19 +340,16 @@ export const SearchSessionIndicator = React.forwardRef< className="searchSessionIndicator" data-test-subj={'searchSessionIndicator'} data-state={props.state} + data-save-disabled={props.saveDisabled ?? false} panelClassName={'searchSessionIndicator__panel'} repositionOnScroll={true} button={ - + } diff --git a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts index 69b3e05946345..bf79d35178a60 100644 --- a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts +++ b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts @@ -47,9 +47,7 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) { public async disabledOrFail() { await this.exists(); - await expect(await (await (await this.find()).findByTagName('button')).isEnabled()).to.be( - false - ); + await expect(await (await this.find()).getAttribute('data-save-disabled')).to.be('true'); } public async expectState(state: SessionStateType) { From 14d41c1952335af4c4b8e93f164939354901bfe9 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 8 Feb 2021 07:47:36 -0800 Subject: [PATCH 012/100] [DOCS] More cleanup in developer docs (#90506) --- docs/developer/contributing/development-ci-metrics.asciidoc | 6 ------ .../getting-started/development-plugin-resources.asciidoc | 4 ++-- src/core/CONVENTIONS.md | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/docs/developer/contributing/development-ci-metrics.asciidoc b/docs/developer/contributing/development-ci-metrics.asciidoc index 9c54ef9c8a916..3e49686fb67f0 100644 --- a/docs/developer/contributing/development-ci-metrics.asciidoc +++ b/docs/developer/contributing/development-ci-metrics.asciidoc @@ -44,15 +44,9 @@ All metrics are collected from the `tar.gz` archive produced for the linux platf [[ci-metric-distributable-file-count]] `distributable file count` :: The number of files included in the default distributable. -[[ci-metric-oss-distributable-file-count]] `oss distributable file count` :: -The number of files included in the OSS distributable. - [[ci-metric-distributable-size]] `distributable size` :: The size, in bytes, of the default distributable. _(not reported on PRs)_ -[[ci-metric-oss-distributable-size]] `oss distributable size` :: -The size, in bytes, of the OSS distributable. _(not reported on PRs)_ - [[ci-metric-types-saved-object-field-counts]] ==== Saved Object field counts diff --git a/docs/developer/getting-started/development-plugin-resources.asciidoc b/docs/developer/getting-started/development-plugin-resources.asciidoc index 863a67f3c42f0..9aefeabb32a55 100644 --- a/docs/developer/getting-started/development-plugin-resources.asciidoc +++ b/docs/developer/getting-started/development-plugin-resources.asciidoc @@ -14,8 +14,8 @@ You can use the <> to get a basic structure for a ne {kib} repo should be developed inside the `plugins` folder. If you are building a new plugin to check in to the {kib} repo, you will choose between a few locations: - - {kib-repo}tree/{branch}/x-pack/plugins[x-pack/plugins] for commercially licensed plugins - - {kib-repo}tree/{branch}/src/plugins[src/plugins] for open source licensed plugins + - {kib-repo}tree/{branch}/x-pack/plugins[x-pack/plugins] for plugins related to subscription features + - {kib-repo}tree/{branch}/src/plugins[src/plugins] for plugins related to free features - {kib-repo}tree/{branch}/examples[examples] for developer example plugins (these will not be included in the distributables) [discrete] diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index a4f50e73f1c57..56da185d023a9 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -19,10 +19,7 @@ Definition of done for a feature: - has been verified manually by at least one reviewer - can be used by first & third party plugins - there is no contradiction between client and server API -- works for OSS version - - works with and without a `server.basePath` configured - - cannot crash the Kibana server when it fails -- works for the commercial version with a license +- works with the subscription features - for a logged-in user - for anonymous user - compatible with Spaces From d804f4ff760832bc20dc29bc299b4b8c92bef3a5 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 8 Feb 2021 11:14:11 -0500 Subject: [PATCH 013/100] Remove extraneous period (#90214) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/sections/alert_form/alert_notify_when.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx index b6676cfeed140..ee0f1c4c0ceb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx @@ -34,7 +34,7 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ inputDisplay: i18n.translate( 'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.display', { - defaultMessage: 'Only on status change.', + defaultMessage: 'Only on status change', } ), 'data-test-subj': 'onActionGroupChange', From 4b29e35246208a6e0aff3dcaef15c7331ae7241a Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 8 Feb 2021 11:16:36 -0500 Subject: [PATCH 014/100] [Alerting] Fixing bug with Index Threshold alert when selecting "Of" expression (#90174) * Fixing bug * Updating functional test * Fixing functional test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../alert_types/threshold/expression.tsx | 12 ++--- .../public/common/expression_items/of.tsx | 2 + .../common/expression_items/when.test.tsx | 2 + .../public/common/expression_items/when.tsx | 1 + .../alert_create_flyout.ts | 54 ++++++++++++++++--- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index 4cccd82673124..aed115a53fa26 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -124,15 +124,13 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< }); if (indexArray.length > 0) { - await refreshEsFields(); + await refreshEsFields(indexArray); } }; - const refreshEsFields = async () => { - if (indexArray.length > 0) { - const currentEsFields = await getFields(http, indexArray); - setEsFields(currentEsFields); - } + const refreshEsFields = async (indices: string[]) => { + const currentEsFields = await getFields(http, indices); + setEsFields(currentEsFields); }; useEffect(() => { @@ -181,7 +179,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< timeField: '', }); } else { - await refreshEsFields(); + await refreshEsFields(indices); } }} onTimeFieldChange={(updatedTimeField: string) => diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx index be54427b90c57..fbc6691455989 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx @@ -91,6 +91,7 @@ export const OfExpression = ({ defaultMessage: 'of', } )} + data-test-subj="ofExpressionPopover" display={display === 'inline' ? 'inline' : 'columns'} value={aggField || firstFieldOption.text} isActive={aggFieldPopoverOpen || !aggField} @@ -119,6 +120,7 @@ export const OfExpression = ({ 0 && aggField !== undefined} error={errors.aggField} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx index cde6980e146b2..d97526d89b62b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx @@ -20,6 +20,7 @@ describe('when expression', () => { { { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index d38ad278d3f64..6a051cc9fc5e6 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -15,6 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const supertest = getService('supertest'); const find = getService('find'); const retry = getService('retry'); + const comboBox = getService('comboBox'); async function getAlertsByName(name: string) { const { @@ -30,15 +31,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } - async function defineAlert(alertName: string, alertType?: string) { - alertType = alertType || '.index-threshold'; + async function defineEsQueryAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); await testSubjects.setValue('alertNameInput', alertName); - await testSubjects.click(`${alertType}-SelectOption`); + await testSubjects.click(`.es-query-SelectOption`); await testSubjects.click('selectIndexExpression'); - const comboBox = await find.byCssSelector('#indexSelectSearchBox'); - await comboBox.click(); - await comboBox.type('k'); + const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); + await indexComboBox.click(); + await indexComboBox.type('k'); const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); await filterSelectItem.click(); await testSubjects.click('thresholdAlertTimeFieldSelect'); @@ -53,6 +53,44 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await nameInput.click(); } + async function defineIndexThresholdAlert(alertName: string) { + await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await testSubjects.setValue('alertNameInput', alertName); + await testSubjects.click(`.index-threshold-SelectOption`); + await testSubjects.click('selectIndexExpression'); + const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); + await indexComboBox.click(); + await indexComboBox.type('k'); + const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); + await filterSelectItem.click(); + await testSubjects.click('thresholdAlertTimeFieldSelect'); + await retry.try(async () => { + const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); + expect(fieldOptions[1]).not.to.be(undefined); + await fieldOptions[1].click(); + }); + await testSubjects.click('closePopover'); + // need this two out of popup clicks to close them + const nameInput = await testSubjects.find('alertNameInput'); + await nameInput.click(); + + await testSubjects.click('whenExpression'); + await testSubjects.click('whenExpressionSelect'); + await retry.try(async () => { + const aggTypeOptions = await find.allByCssSelector('#aggTypeField option'); + expect(aggTypeOptions[1]).not.to.be(undefined); + await aggTypeOptions[1].click(); + }); + + await testSubjects.click('ofExpressionPopover'); + const ofComboBox = await find.byCssSelector('#ofField'); + await ofComboBox.click(); + const ofOptionsString = await comboBox.getOptionsList('availablefieldsOptionsComboBox'); + const ofOptions = ofOptionsString.trim().split('\n'); + expect(ofOptions.length > 0).to.be(true); + await comboBox.set('availablefieldsOptionsComboBox', ofOptions[0]); + } + async function defineAlwaysFiringAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); await testSubjects.setValue('alertNameInput', alertName); @@ -67,7 +105,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should create an alert', async () => { const alertName = generateUniqueKey(); - await defineAlert(alertName); + await defineIndexThresholdAlert(alertName); await testSubjects.click('notifyWhenSelect'); await testSubjects.click('onThrottleInterval'); @@ -222,7 +260,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should successfully test valid es_query alert', async () => { const alertName = generateUniqueKey(); - await defineAlert(alertName, '.es-query'); + await defineEsQueryAlert(alertName); // Valid query await testSubjects.setValue('queryJsonEditor', '{"query":{"match_all":{}}}', { From f6a8d6edc472797482c74b9b369d07fb0475bf43 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 8 Feb 2021 18:45:31 +0200 Subject: [PATCH 015/100] [Security Solution][Case] Fix unhandled promise when updating alert status (#90605) --- x-pack/plugins/case/server/client/comments/add.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 5cfa4d70290f0..58d7c9abcbfd3 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -87,7 +87,7 @@ export const addComment = ({ // If the case is synced with alerts the newly attached alert must match the status of the case. if (newComment.attributes.type === CommentType.alert && myCase.attributes.settings.syncAlerts) { - caseClient.updateAlertsStatus({ + await caseClient.updateAlertsStatus({ ids: [newComment.attributes.alertId], status: myCase.attributes.status, }); From ec672f5df22e16a1a2ba97d38ec04774c3592a57 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 8 Feb 2021 17:48:14 +0100 Subject: [PATCH 016/100] [ML] Handle invalid job ids payload in the Anomaly swim lane (#90597) * [ML] handle invalid job ids payload * [ML] set type for error * [ML] set entire error object --- .../swimlane_input_resolver.test.ts | 36 ++++++++++++++++--- .../swimlane_input_resolver.ts | 16 +++++++-- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts index 2576e5377b39d..3fffd1588b9b9 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts @@ -57,14 +57,17 @@ describe('useSwimlaneInputResolver', () => { ), }, anomalyDetectorService: { - getJobs$: jest.fn(() => - of([ + getJobs$: jest.fn((jobId: string[]) => { + if (jobId.includes('invalid-job-id')) { + throw new Error('Invalid job'); + } + return of([ { job_id: 'cw_multi_1', analysis_config: { bucket_span: '15m' }, }, - ]) - ), + ]); + }), }, } as unknown) as AnomalySwimlaneServices, ]; @@ -128,6 +131,31 @@ describe('useSwimlaneInputResolver', () => { expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2); expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(3); }); + + test('should not complete the observable on error', async () => { + const { result } = renderHook(() => + useSwimlaneInputResolver( + embeddableInput as Observable, + onInputChange, + refresh, + services, + 1000, + 1 + ) + ); + + await act(async () => { + embeddableInput.next({ + id: 'test-swimlane-embeddable', + jobIds: ['invalid-job-id'], + swimlaneType: SWIMLANE_TYPE.OVERALL, + filters: [], + query: { language: 'kuery', query: '' }, + } as Partial); + }); + + expect(result.current[6]?.message).toBe('Invalid job'); + }); }); describe('processFilters', () => { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts index 5b256b9c5924c..0d75db64a01b9 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts @@ -47,12 +47,17 @@ const FETCH_RESULTS_DEBOUNCE_MS = 500; function getJobsObservable( embeddableInput: Observable, - anomalyDetectorService: AnomalyDetectorService + anomalyDetectorService: AnomalyDetectorService, + setErrorHandler: (e: Error) => void ) { return embeddableInput.pipe( pluck('jobIds'), distinctUntilChanged(isEqual), - switchMap((jobsIds) => anomalyDetectorService.getJobs$(jobsIds)) + switchMap((jobsIds) => anomalyDetectorService.getJobs$(jobsIds)), + catchError((e) => { + setErrorHandler(e.body ?? e); + return of(undefined); + }) ); } @@ -95,7 +100,7 @@ export function useSwimlaneInputResolver( useEffect(() => { const subscription = combineLatest([ - getJobsObservable(embeddableInput, anomalyDetectorService), + getJobsObservable(embeddableInput, anomalyDetectorService, setError), embeddableInput, chartWidth$.pipe(skipWhile((v) => !v)), fromPage$, @@ -112,6 +117,11 @@ export function useSwimlaneInputResolver( tap(setIsLoading.bind(null, true)), debounceTime(FETCH_RESULTS_DEBOUNCE_MS), switchMap(([jobs, input, swimlaneContainerWidth, fromPageInput, perPageFromState]) => { + if (!jobs) { + // couldn't load the list of jobs + return of(undefined); + } + const { viewBy, swimlaneType: swimlaneTypeInput, From bda7b2816f00d288aee774fc3661ed022bd0f270 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Mon, 8 Feb 2021 12:13:55 -0500 Subject: [PATCH 017/100] [Fleet] Cannot delete a managed agent policy (#90505) ## Summary Managed policy cannot be deleted via API or UI closes https://github.com/elastic/kibana/issues/90448 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios #### Manual testing
UI screenshot Screen Shot 2021-02-05 at 1 56 13 PM
API commands ``` ## Create a managed policy curl --user elastic:changeme -X POST localhost:5601/api/fleet/agent_policies -H 'Content-Type: application/json' -d'{ "name": "User created MANAGED", "namespace": "default", "is_managed": true}' -H 'kbn-xsrf: true' {"item":{"id":"17ebd160-67ee-11eb-adb2-f16c6e20580c","name":"User created MANAGED","namespace":"default","is_managed":true,"revision":1,"updated_at":"2021-02-05T20:09:46.614Z","updated_by":"elastic"}} ## Cannot delete it curl --user elastic:changeme -X POST 'http://localhost:5601/api/fleet/agent_policies/delete' -H 'kbn-xsrf: abc' -H 'Content-Type: application/json' --data-raw '{"agentPolicyId": "17ebd160-67ee-11eb-adb2-f16c6e20580c" }' { "statusCode": 400, "error": "Bad Request", "message": "Cannot delete managed policy 17ebd160-67ee-11eb-adb2-f16c6e20580c" } ## Set policy to unmanaged curl --user elastic:changeme -X PUT localhost:5601/api/fleet/agent_policies/17ebd160-67ee-11eb-adb2-f16c6e20580c -H 'Content-Type: application/json' -d'{ "name": "User created MANAGED", "namespace": "default", "is_managed": false}' -H 'kbn-xsrf: true' { "item": { "id": "17ebd160-67ee-11eb-adb2-f16c6e20580c", "name": "User created MANAGED", "namespace": "default", "is_managed": false, "revision": 3, "updated_at": "2021-02-05T20:10:45.383Z", "updated_by": "elastic", "package_policies": [] } } ## Can delete curl --user elastic:changeme -X POST 'http://localhost:5601/api/fleet/agent_policies/delete' -H 'kbn-xsrf: abc' -H 'Content-Type: application/json' --data-raw '{"agentPolicyId": "17ebd160-67ee-11eb-adb2-f16c6e20580c" }' { "id": "17ebd160-67ee-11eb-adb2-f16c6e20580c", "name": "User created MANAGED" } ```
--- x-pack/plugins/fleet/server/errors/index.ts | 1 + .../fleet/server/services/agent_policy.ts | 6 +- .../apis/agent_policy/agent_policy.ts | 93 +++++++++++++++++-- 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index a903de0138039..b34568b5fc6af 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -34,3 +34,4 @@ export class FleetAdminUserInvalidError extends IngestManagerError {} export class ConcurrentInstallOperationError extends IngestManagerError {} export class AgentReassignmentError extends IngestManagerError {} export class AgentUnenrollmentError extends IngestManagerError {} +export class AgentPolicyDeletionError extends IngestManagerError {} diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index ca131efeff68c..9800ddf95f7b2 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -36,7 +36,7 @@ import { FleetServerPolicy, AGENT_POLICY_INDEX, } from '../../common'; -import { AgentPolicyNameExistsError } from '../errors'; +import { AgentPolicyNameExistsError, AgentPolicyDeletionError } from '../errors'; import { createAgentPolicyAction, listAgents } from './agents'; import { packagePolicyService } from './package_policy'; import { outputService } from './output'; @@ -448,6 +448,10 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } + if (agentPolicy.is_managed) { + throw new AgentPolicyDeletionError(`Cannot delete managed policy ${id}`); + } + const { defaultAgentPolicy: { id: defaultAgentPolicyId }, } = await this.ensureDefaultAgentPolicy(soClient, esClient); diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index 9f016ab044a90..2ba83bff6f1b1 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -38,9 +38,8 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(200); - const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); - const json = getRes.body; - expect(json.item.is_managed).to.equal(false); + const { body } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); + expect(body.item.is_managed).to.equal(false); }); it('sets given is_managed value', async () => { @@ -56,9 +55,25 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(200); - const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); - const json = getRes.body; - expect(json.item.is_managed).to.equal(true); + const { body } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); + expect(body.item.is_managed).to.equal(true); + + const { + body: { item: createdPolicy2 }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST3', + namespace: 'default', + is_managed: false, + }) + .expect(200); + + const { + body: { item: policy2 }, + } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy2.id}`); + expect(policy2.is_managed).to.equal(false); }); it('should return a 400 with an empty namespace', async () => { @@ -242,6 +257,23 @@ export default function ({ getService }: FtrProviderContext) { const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); const json = getRes.body; expect(json.item.is_managed).to.equal(true); + + const { + body: { item: createdPolicy2 }, + } = await supertest + .put(`/api/fleet/agent_policies/${agentPolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST2', + namespace: 'default', + is_managed: false, + }) + .expect(200); + + const { + body: { item: policy2 }, + } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy2.id}`); + expect(policy2.is_managed).to.equal(false); }); it('should return a 409 if policy already exists with name given', async () => { @@ -276,5 +308,54 @@ export default function ({ getService }: FtrProviderContext) { expect(body.message).to.match(/already exists?/); }); }); + + describe('POST /api/fleet/agent_policies/delete', () => { + let managedPolicy: any | undefined; + it('should prevent managed policies being deleted', async () => { + const { + body: { item: createdPolicy }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Managed policy', + namespace: 'default', + is_managed: true, + }) + .expect(200); + managedPolicy = createdPolicy; + const { body } = await supertest + .post('/api/fleet/agent_policies/delete') + .set('kbn-xsrf', 'xxx') + .send({ agentPolicyId: managedPolicy.id }) + .expect(400); + + expect(body.message).to.contain('Cannot delete managed policy'); + }); + + it('should allow unmanaged policies being deleted', async () => { + const { + body: { item: unmanagedPolicy }, + } = await supertest + .put(`/api/fleet/agent_policies/${managedPolicy.id}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Unmanaged policy', + namespace: 'default', + is_managed: false, + }) + .expect(200); + + const { body } = await supertest + .post('/api/fleet/agent_policies/delete') + .set('kbn-xsrf', 'xxx') + .send({ agentPolicyId: unmanagedPolicy.id }); + + expect(body).to.eql({ + id: unmanagedPolicy.id, + name: 'Unmanaged policy', + }); + }); + }); }); } From c306a444f5550faee08d40612386e52731fc657f Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Mon, 8 Feb 2021 18:22:30 +0100 Subject: [PATCH 018/100] [EPM] Conditionally generate ES index pattern name based on dataset_is_prefix (#89870) * Explicitly generate ES index pattern name. * Adjust tests. * Adjust and reenable tests. * Set template priority based on dataset_is_prefix * Refactor indexPatternName -> templateIndexPattern * Add unit tests. * Use more realistic index pattern in test. * Fix unit test. * Add unit test for installTemplate(). Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/common/types/models/epm.ts | 1 + .../elasticsearch/template/install.test.ts | 110 ++++++++++++++++++ .../epm/elasticsearch/template/install.ts | 14 ++- .../elasticsearch/template/template.test.ts | 99 +++++++++++++--- .../epm/elasticsearch/template/template.ts | 55 +++++++-- .../fleet_api_integration/apis/epm/index.js | 2 +- .../apis/epm/template.ts | 21 +++- 7 files changed, 268 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 0f59befc2e467..e7e5a931b7429 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -221,6 +221,7 @@ export interface RegistryDataStream { path: string; ingest_pipeline: string; elasticsearch?: RegistryElasticsearch; + dataset_is_prefix?: boolean; } export interface RegistryElasticsearch { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts new file mode 100644 index 0000000000000..be9213aff360d --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RegistryDataStream } from '../../../../types'; +import { Field } from '../../fields/field'; + +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { installTemplate } from './install'; + +test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + const fields: Field[] = []; + const dataStreamDatasetIsPrefixUnset = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixUnset = 200; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixUnset, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[0][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); +}); + +test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + const fields: Field[] = []; + const dataStreamDatasetIsPrefixFalse = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: false, + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixFalse = 200; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixFalse, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[0][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixFalse); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]); +}); + +test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + const fields: Field[] = []; + const dataStreamDatasetIsPrefixTrue = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: true, + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; + const templatePriorityDatasetIsPrefixTrue = 150; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixTrue, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[0][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixTrue); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 10e94d93bbc8e..f5f1b4bea788d 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -17,7 +17,13 @@ import { import { CallESAsCurrentUser } from '../../../../types'; import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; -import { generateMappings, generateTemplateName, getTemplate } from './template'; +import { + generateMappings, + generateTemplateName, + generateTemplateIndexPattern, + getTemplate, + getTemplatePriority, +} from './template'; import { getAsset, getPathParts } from '../../archive'; import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install'; @@ -293,6 +299,9 @@ export async function installTemplate({ }): Promise { const mappings = generateMappings(processFields(fields)); const templateName = generateTemplateName(dataStream); + const templateIndexPattern = generateTemplateIndexPattern(dataStream); + const templatePriority = getTemplatePriority(dataStream); + let pipelineName; if (dataStream.ingest_pipeline) { pipelineName = getPipelineNameForInstallation({ @@ -310,11 +319,12 @@ export async function installTemplate({ const template = getTemplate({ type: dataStream.type, - templateName, + templateIndexPattern, mappings, pipelineName, packageName, composedOfTemplates, + templatePriority, ilmPolicy: dataStream.ilm_policy, hidden: dataStream.hidden, }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index 80386a2a0dd56..a176805307845 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -8,8 +8,14 @@ import { readFileSync } from 'fs'; import { safeLoad } from 'js-yaml'; import path from 'path'; +import { RegistryDataStream } from '../../../../types'; import { Field, processFields } from '../../fields/field'; -import { generateMappings, getTemplate } from './template'; +import { + generateMappings, + getTemplate, + getTemplatePriority, + generateTemplateIndexPattern, +} from './template'; // Add our own serialiser to just do JSON.stringify expect.addSnapshotSerializer({ @@ -23,16 +29,17 @@ expect.addSnapshotSerializer({ }); test('get template', () => { - const templateName = 'logs-nginx-access-abcd'; + const templateIndexPattern = 'logs-nginx.access-abcd-*'; const template = getTemplate({ type: 'logs', - templateName, + templateIndexPattern, packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates: [], + templatePriority: 200, }); - expect(template.index_patterns).toStrictEqual([`${templateName}-*`]); + expect(template.index_patterns).toStrictEqual([templateIndexPattern]); }); test('adds composed_of correctly', () => { @@ -40,10 +47,11 @@ test('adds composed_of correctly', () => { const template = getTemplate({ type: 'logs', - templateName: 'name', + templateIndexPattern: 'name-*', packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates, + templatePriority: 200, }); expect(template.composed_of).toStrictEqual(composedOfTemplates); }); @@ -53,35 +61,36 @@ test('adds empty composed_of correctly', () => { const template = getTemplate({ type: 'logs', - templateName: 'name', + templateIndexPattern: 'name-*', packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates, + templatePriority: 200, }); expect(template.composed_of).toStrictEqual(composedOfTemplates); }); test('adds hidden field correctly', () => { - const templateWithHiddenName = 'logs-nginx-access-abcd'; + const templateIndexPattern = 'logs-nginx.access-abcd-*'; const templateWithHidden = getTemplate({ type: 'logs', - templateName: templateWithHiddenName, + templateIndexPattern, packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates: [], + templatePriority: 200, hidden: true, }); expect(templateWithHidden.data_stream.hidden).toEqual(true); - const templateWithoutHiddenName = 'logs-nginx-access-efgh'; - const templateWithoutHidden = getTemplate({ type: 'logs', - templateName: templateWithoutHiddenName, + templateIndexPattern, packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates: [], + templatePriority: 200, }); expect(templateWithoutHidden.data_stream.hidden).toEqual(undefined); }); @@ -95,10 +104,11 @@ test('tests loading base.yml', () => { const mappings = generateMappings(processedFields); const template = getTemplate({ type: 'logs', - templateName: 'foo', + templateIndexPattern: 'foo-*', packageName: 'nginx', mappings, composedOfTemplates: [], + templatePriority: 200, }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -113,10 +123,11 @@ test('tests loading coredns.logs.yml', () => { const mappings = generateMappings(processedFields); const template = getTemplate({ type: 'logs', - templateName: 'foo', + templateIndexPattern: 'foo-*', packageName: 'coredns', mappings, composedOfTemplates: [], + templatePriority: 200, }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -131,10 +142,11 @@ test('tests loading system.yml', () => { const mappings = generateMappings(processedFields); const template = getTemplate({ type: 'metrics', - templateName: 'whatsthis', + templateIndexPattern: 'whatsthis-*', packageName: 'system', mappings, composedOfTemplates: [], + templatePriority: 200, }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -520,3 +532,62 @@ test('tests constant_keyword field type handling', () => { const mappings = generateMappings(processedFields); expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping)); }); + +test('tests priority and index pattern for data stream without dataset_is_prefix', () => { + const dataStreamDatasetIsPrefixUnset = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixUnset = 200; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixUnset); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixUnset); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixUnset); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixUnset); +}); + +test('tests priority and index pattern for data stream with dataset_is_prefix set to false', () => { + const dataStreamDatasetIsPrefixFalse = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: false, + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixFalse = 200; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixFalse); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixFalse); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixFalse); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixFalse); +}); + +test('tests priority and index pattern for data stream with dataset_is_prefix set to true', () => { + const dataStreamDatasetIsPrefixTrue = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: true, + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; + const templatePriorityDatasetIsPrefixTrue = 150; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixTrue); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixTrue); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixTrue); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixTrue); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index ea0bb5dc53a1e..b86c989f8c24c 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -33,6 +33,10 @@ export interface CurrentDataStream { const DEFAULT_SCALING_FACTOR = 1000; const DEFAULT_IGNORE_ABOVE = 1024; +// see discussion in https://github.com/elastic/kibana/issues/88307 +const DEFAULT_TEMPLATE_PRIORITY = 200; +const DATASET_IS_PREFIX_TEMPLATE_PRIORITY = 150; + /** * getTemplate retrieves the default template but overwrites the index pattern with the given value. * @@ -40,29 +44,32 @@ const DEFAULT_IGNORE_ABOVE = 1024; */ export function getTemplate({ type, - templateName, + templateIndexPattern, mappings, pipelineName, packageName, composedOfTemplates, + templatePriority, ilmPolicy, hidden, }: { type: string; - templateName: string; + templateIndexPattern: string; mappings: IndexTemplateMappings; pipelineName?: string | undefined; packageName: string; composedOfTemplates: string[]; + templatePriority: number; ilmPolicy?: string | undefined; hidden?: boolean; }): IndexTemplate { const template = getBaseTemplate( type, - templateName, + templateIndexPattern, mappings, packageName, composedOfTemplates, + templatePriority, ilmPolicy, hidden ); @@ -242,6 +249,35 @@ export function generateTemplateName(dataStream: RegistryDataStream): string { return getRegistryDataStreamAssetBaseName(dataStream); } +export function generateTemplateIndexPattern(dataStream: RegistryDataStream): string { + // undefined or explicitly set to false + // See also https://github.com/elastic/package-spec/pull/102 + if (!dataStream.dataset_is_prefix) { + return getRegistryDataStreamAssetBaseName(dataStream) + '-*'; + } else { + return getRegistryDataStreamAssetBaseName(dataStream) + '.*-*'; + } +} + +// Template priorities are discussed in https://github.com/elastic/kibana/issues/88307 +// See also https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html +// +// Built-in templates like logs-*-* and metrics-*-* have priority 100 +// +// EPM generated templates for data streams have priority 200 (DEFAULT_TEMPLATE_PRIORITY) +// +// EPM generated templates for data streams with dataset_is_prefix: true have priority 150 (DATASET_IS_PREFIX_TEMPLATE_PRIORITY) + +export function getTemplatePriority(dataStream: RegistryDataStream): number { + // undefined or explicitly set to false + // See also https://github.com/elastic/package-spec/pull/102 + if (!dataStream.dataset_is_prefix) { + return DEFAULT_TEMPLATE_PRIORITY; + } else { + return DATASET_IS_PREFIX_TEMPLATE_PRIORITY; + } +} + /** * Returns a map of the data stream path fields to elasticsearch index pattern. * @param dataStreams an array of RegistryDataStream objects @@ -255,17 +291,18 @@ export function generateESIndexPatterns( const patterns: Record = {}; for (const dataStream of dataStreams) { - patterns[dataStream.path] = generateTemplateName(dataStream) + '-*'; + patterns[dataStream.path] = generateTemplateIndexPattern(dataStream); } return patterns; } function getBaseTemplate( type: string, - templateName: string, + templateIndexPattern: string, mappings: IndexTemplateMappings, packageName: string, composedOfTemplates: string[], + templatePriority: number, ilmPolicy?: string | undefined, hidden?: boolean ): IndexTemplate { @@ -279,13 +316,9 @@ function getBaseTemplate( }; return { - // This takes precedence over all index templates installed by ES by default (logs-*-* and metrics-*-*) - // if this number is lower than the ES value (which is 100) this template will never be applied when a data stream - // is created. I'm using 200 here to give some room for users to create their own template and fit it between the - // default and the one the ingest manager uses. - priority: 200, + priority: templatePriority, // To be completed with the correct index patterns - index_patterns: [`${templateName}-*`], + index_patterns: [templateIndexPattern], template: { settings: { index: { diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index 23b7464a317e9..0020e6bdf1bb0 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -11,7 +11,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./setup')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./file')); - //loadTestFile(require.resolve('./template')); + loadTestFile(require.resolve('./template')); loadTestFile(require.resolve('./ilm')); loadTestFile(require.resolve('./install_by_upload')); loadTestFile(require.resolve('./install_overrides')); diff --git a/x-pack/test/fleet_api_integration/apis/epm/template.ts b/x-pack/test/fleet_api_integration/apis/epm/template.ts index c7e9e21155257..d79452ca0eb6f 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/template.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/template.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../api_integration/ftr_provider_contex import { getTemplate } from '../../../../plugins/fleet/server/services/epm/elasticsearch/template/template'; export default function ({ getService }: FtrProviderContext) { - const indexPattern = 'foo'; const templateName = 'bar'; + const templateIndexPattern = 'bar-*'; const es = getService('es'); const mappings = { properties: { @@ -25,27 +25,36 @@ export default function ({ getService }: FtrProviderContext) { it('can be loaded', async () => { const template = getTemplate({ type: 'logs', - templateName, + templateIndexPattern, mappings, packageName: 'system', composedOfTemplates: [], + templatePriority: 200, }); // This test is not an API integration test with Kibana // We want to test here if the template is valid and for this we need a running ES instance. // If the ES instance takes the template, we assume it is a valid template. - const { body: response1 } = await es.indices.putTemplate({ - name: templateName, + const { body: response1 } = await es.transport.request({ + method: 'PUT', + path: `/_index_template/${templateName}`, body: template, }); + // Checks if template loading worked as expected expect(response1).to.eql({ acknowledged: true }); - const { body: response2 } = await es.indices.getTemplate({ name: templateName }); + const { body: response2 } = await es.transport.request({ + method: 'GET', + path: `/_index_template/${templateName}`, + }); + // Checks if the content of the template that was loaded is as expected // We already know based on the above test that the template was valid // but we check here also if we wrote the index pattern inside the template as expected - expect(response2[templateName].index_patterns).to.eql([`${indexPattern}-*`]); + expect(response2.index_templates[0].index_template.index_patterns).to.eql([ + templateIndexPattern, + ]); }); }); } From a0ce7b5aa887d34a7a892553c66f25d72e38d827 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 8 Feb 2021 09:47:55 -0800 Subject: [PATCH 019/100] [kbn/optimizer][ci-stats] ship metrics separate from build (#90482) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 + .../src/ci_stats_reporter/index.ts | 1 + .../ci_stats_reporter/ship_ci_stats_cli.ts | 48 +++++++ packages/kbn-optimizer/src/cli.ts | 17 +-- .../kbn-optimizer/src/common/bundle.test.ts | 2 + packages/kbn-optimizer/src/common/bundle.ts | 16 ++- .../src/common/bundle_cache.test.ts | 14 +- .../kbn-optimizer/src/common/bundle_cache.ts | 22 ++- packages/kbn-optimizer/src/index.ts | 1 - .../basic_optimization.test.ts.snap | 35 ++++- .../basic_optimization.test.ts | 15 +- packages/kbn-optimizer/src/limits.ts | 21 ++- .../src/optimizer/get_output_stats.ts | 118 ---------------- .../src/optimizer/get_plugin_bundles.test.ts | 10 +- .../src/optimizer/get_plugin_bundles.ts | 5 +- packages/kbn-optimizer/src/optimizer/index.ts | 1 - .../src/optimizer/optimizer_config.test.ts | 8 +- .../src/optimizer/optimizer_config.ts | 10 +- .../src/report_optimizer_stats.ts | 46 ------ .../src/worker/bundle_metrics_plugin.ts | 108 ++++++++++++++ .../src/worker/emit_stats_plugin.ts | 34 +++++ .../worker/populate_bundle_cache_plugin.ts | 132 ++++++++++++++++++ .../kbn-optimizer/src/worker/run_compilers.ts | 122 +--------------- .../src/worker/webpack.config.ts | 6 + .../src/integration_tests/build.test.ts | 3 +- .../kbn-plugin-helpers/src/tasks/optimize.ts | 8 +- scripts/ship_ci_stats.js | 10 ++ .../tasks/build_kibana_platform_plugins.ts | 39 ++++-- test/scripts/jenkins_baseline.sh | 4 + test/scripts/jenkins_build_kibana.sh | 3 + test/scripts/jenkins_xpack_baseline.sh | 4 + test/scripts/jenkins_xpack_build_kibana.sh | 4 + yarn.lock | 2 +- 33 files changed, 518 insertions(+), 353 deletions(-) create mode 100644 packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts delete mode 100644 packages/kbn-optimizer/src/optimizer/get_output_stats.ts delete mode 100644 packages/kbn-optimizer/src/report_optimizer_stats.ts create mode 100644 packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts create mode 100644 packages/kbn-optimizer/src/worker/emit_stats_plugin.ts create mode 100644 packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts create mode 100644 scripts/ship_ci_stats.js diff --git a/package.json b/package.json index b224f0c1ae0d5..7144745f2ae35 100644 --- a/package.json +++ b/package.json @@ -558,6 +558,7 @@ "@types/webpack": "^4.41.3", "@types/webpack-env": "^1.15.3", "@types/webpack-merge": "^4.1.5", + "@types/webpack-sources": "^0.1.4", "@types/write-pkg": "^3.1.0", "@types/xml-crypto": "^1.4.1", "@types/xml2js": "^0.4.5", @@ -843,6 +844,7 @@ "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0", "webpack-merge": "^4.2.2", + "webpack-sources": "^1.4.1", "write-pkg": "^4.0.0", "xml-crypto": "^2.0.0", "xmlbuilder": "13.0.2", diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts index 165239cbebb89..d99217c38b410 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts @@ -7,3 +7,4 @@ */ export * from './ci_stats_reporter'; +export * from './ship_ci_stats_cli'; diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts new file mode 100644 index 0000000000000..244af7b657418 --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Fs from 'fs'; + +import { CiStatsReporter } from './ci_stats_reporter'; +import { run, createFlagError } from '../run'; + +export function shipCiStatsCli() { + run( + async ({ log, flags }) => { + let metricPaths = flags.metrics; + if (typeof metricPaths === 'string') { + metricPaths = [metricPaths]; + } else if (!Array.isArray(metricPaths) || !metricPaths.every((p) => typeof p === 'string')) { + throw createFlagError('expected --metrics to be a string'); + } + + const reporter = CiStatsReporter.fromEnv(log); + for (const path of metricPaths) { + // resolve path from CLI relative to CWD + const abs = Path.resolve(path); + const json = Fs.readFileSync(abs, 'utf8'); + await reporter.metrics(JSON.parse(json)); + log.success('shipped metrics from', path); + } + }, + { + description: 'ship ci-stats which have been written to files', + usage: `node scripts/ship_ci_stats`, + log: { + defaultLevel: 'debug', + }, + flags: { + string: ['metrics'], + help: ` + --metrics [path] A path to a JSON file that includes metrics which should be sent. Multiple instances supported + `, + }, + } + ); +} diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 3021982b8ed6a..8fb906aa4603e 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -12,11 +12,10 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { lastValueFrom } from '@kbn/std'; -import { run, createFlagError, CiStatsReporter } from '@kbn/dev-utils'; +import { run, createFlagError } from '@kbn/dev-utils'; import { logOptimizerState } from './log_optimizer_state'; import { OptimizerConfig } from './optimizer'; -import { reportOptimizerStats } from './report_optimizer_stats'; import { runOptimizer } from './run_optimizer'; import { validateLimitsForAllBundles, updateBundleLimits } from './limits'; @@ -120,17 +119,7 @@ run( return; } - let update$ = runOptimizer(config); - - if (reportStats) { - const reporter = CiStatsReporter.fromEnv(log); - - if (!reporter.isEnabled()) { - log.warning('Unable to initialize CiStatsReporter from env'); - } - - update$ = update$.pipe(reportOptimizerStats(reporter, config, log)); - } + const update$ = runOptimizer(config); await lastValueFrom(update$.pipe(logOptimizerState(log, config))); @@ -153,7 +142,6 @@ run( 'cache', 'profile', 'inspect-workers', - 'report-stats', 'validate-limits', 'update-limits', ], @@ -179,7 +167,6 @@ run( --dist create bundles that are suitable for inclusion in the Kibana distributable, enabled when running with --update-limits --scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary) --no-inspect-workers when inspecting the parent process, don't inspect the workers - --report-stats attempt to report stats about this execution of the build to the kibana-ci-stats service using this name --validate-limits validate the limits.yml config to ensure that there are limits defined for every bundle --update-limits run a build and rewrite the limits file to include the current bundle sizes +5kb `, diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts index b6d25f69e58b4..ff9aa6fd90628 100644 --- a/packages/kbn-optimizer/src/common/bundle.test.ts +++ b/packages/kbn-optimizer/src/common/bundle.test.ts @@ -42,6 +42,7 @@ it('creates cache keys', () => { "id": "bar", "manifestPath": undefined, "outputDir": "/foo/bar/target", + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -79,6 +80,7 @@ it('parses bundles from JSON specs', () => { "id": "bar", "manifestPath": undefined, "outputDir": "/foo/bar/target", + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index cb6096759739b..64b44de0dd1b3 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -36,6 +36,8 @@ export interface BundleSpec { readonly banner?: string; /** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */ readonly manifestPath?: string; + /** Maximum allowed page load asset size for the bundles page load asset */ + readonly pageLoadAssetSizeLimit?: number; } export class Bundle { @@ -63,6 +65,8 @@ export class Bundle { * Every bundle mentioned in the `requiredBundles` must be built together. */ public readonly manifestPath: BundleSpec['manifestPath']; + /** Maximum allowed page load asset size for the bundles page load asset */ + public readonly pageLoadAssetSizeLimit: BundleSpec['pageLoadAssetSizeLimit']; public readonly cache: BundleCache; @@ -75,8 +79,9 @@ export class Bundle { this.outputDir = spec.outputDir; this.manifestPath = spec.manifestPath; this.banner = spec.banner; + this.pageLoadAssetSizeLimit = spec.pageLoadAssetSizeLimit; - this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache')); + this.cache = new BundleCache(this.outputDir); } /** @@ -107,6 +112,7 @@ export class Bundle { outputDir: this.outputDir, manifestPath: this.manifestPath, banner: this.banner, + pageLoadAssetSizeLimit: this.pageLoadAssetSizeLimit, }; } @@ -222,6 +228,13 @@ export function parseBundles(json: string) { } } + const { pageLoadAssetSizeLimit } = spec; + if (pageLoadAssetSizeLimit !== undefined) { + if (!(typeof pageLoadAssetSizeLimit === 'number')) { + throw new Error('`bundles[]` must have a numeric `pageLoadAssetSizeLimit` property'); + } + } + return new Bundle({ type, id, @@ -231,6 +244,7 @@ export function parseBundles(json: string) { outputDir, banner, manifestPath, + pageLoadAssetSizeLimit, }); } ); diff --git a/packages/kbn-optimizer/src/common/bundle_cache.test.ts b/packages/kbn-optimizer/src/common/bundle_cache.test.ts index 82a8c0debb83c..e903a687908b9 100644 --- a/packages/kbn-optimizer/src/common/bundle_cache.test.ts +++ b/packages/kbn-optimizer/src/common/bundle_cache.test.ts @@ -25,12 +25,12 @@ beforeEach(() => { }); it(`doesn't complain if files are not on disk`, () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); expect(cache.get()).toEqual({}); }); it(`updates files on disk when calling set()`, () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); cache.set(SOME_STATE); expect(mockReadFileSync).not.toHaveBeenCalled(); expect(mockMkdirSync.mock.calls).toMatchInlineSnapshot(` @@ -46,7 +46,7 @@ it(`updates files on disk when calling set()`, () => { expect(mockWriteFileSync.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/foo/bar.json", + "/foo/.kbn-optimizer-cache", "{ \\"cacheKey\\": \\"abc\\", \\"files\\": [ @@ -61,7 +61,7 @@ it(`updates files on disk when calling set()`, () => { }); it(`serves updated state from memory`, () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); cache.set(SOME_STATE); jest.clearAllMocks(); @@ -72,7 +72,7 @@ it(`serves updated state from memory`, () => { }); it('reads state from disk on get() after refresh()', () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); cache.set(SOME_STATE); cache.refresh(); jest.clearAllMocks(); @@ -83,7 +83,7 @@ it('reads state from disk on get() after refresh()', () => { expect(mockReadFileSync.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/foo/bar.json", + "/foo/.kbn-optimizer-cache", "utf8", ], ] @@ -91,7 +91,7 @@ it('reads state from disk on get() after refresh()', () => { }); it('provides accessors to specific state properties', () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); expect(cache.getModuleCount()).toBe(undefined); expect(cache.getReferencedFiles()).toEqual(undefined); diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts index 39b52095c819a..7c0770caa2623 100644 --- a/packages/kbn-optimizer/src/common/bundle_cache.ts +++ b/packages/kbn-optimizer/src/common/bundle_cache.ts @@ -9,6 +9,9 @@ import Fs from 'fs'; import Path from 'path'; +import webpack from 'webpack'; +import { RawSource } from 'webpack-sources'; + export interface State { optimizerCacheKey?: unknown; cacheKey?: unknown; @@ -20,13 +23,17 @@ export interface State { const DEFAULT_STATE: State = {}; const DEFAULT_STATE_JSON = JSON.stringify(DEFAULT_STATE); +const CACHE_FILENAME = '.kbn-optimizer-cache'; /** * Helper to read and update metadata for bundles. */ export class BundleCache { private state: State | undefined = undefined; - constructor(private readonly path: string | false) {} + private readonly path: string | false; + constructor(outputDir: string | false) { + this.path = outputDir === false ? false : Path.resolve(outputDir, CACHE_FILENAME); + } refresh() { this.state = undefined; @@ -63,6 +70,7 @@ export class BundleCache { set(updated: State) { this.state = updated; + if (this.path) { const directory = Path.dirname(this.path); Fs.mkdirSync(directory, { recursive: true }); @@ -107,4 +115,16 @@ export class BundleCache { } } } + + public writeWebpackAsset(compilation: webpack.compilation.Compilation) { + if (!this.path) { + return; + } + + const source = new RawSource(JSON.stringify(this.state, null, 2)); + + // see https://github.com/jantimon/html-webpack-plugin/blob/33d69f49e6e9787796402715d1b9cd59f80b628f/index.js#L266 + // @ts-expect-error undocumented, used to add assets to the output + compilation.emitAsset(CACHE_FILENAME, source); + } } diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index a74679bfff536..551d2ffacfcfb 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -9,6 +9,5 @@ export { OptimizerConfig } from './optimizer'; export * from './run_optimizer'; export * from './log_optimizer_state'; -export * from './report_optimizer_stats'; export * from './node'; export * from './limits'; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 1ed1b92f9c2d9..9e9e8960da21b 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -13,6 +13,7 @@ OptimizerConfig { "id": "bar", "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -29,6 +30,7 @@ OptimizerConfig { "id": "foo", "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json, "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -47,6 +49,7 @@ OptimizerConfig { "id": "baz", "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -57,7 +60,6 @@ OptimizerConfig { "cache": true, "dist": false, "inspectWorkers": false, - "limits": "", "maxWorkerCount": 1, "plugins": Array [ Object { @@ -109,3 +111,34 @@ exports[`prepares assets for distribution: baz bundle 1`] = ` exports[`prepares assets for distribution: foo async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}}]);"`; exports[`prepares assets for distribution: foo bundle 1`] = `"(function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i { dist: false, }); - expect(config.limits).toEqual(readLimits()); - (config as any).limits = ''; - expect(config).toMatchSnapshot('OptimizerConfig'); const msgs = await allValuesFrom( @@ -235,6 +226,10 @@ it('prepares assets for distribution', async () => { await allValuesFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); + expect( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/metrics.json'), 'utf8') + ).toMatchSnapshot('metrics.json'); + expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); expectFileMatchesSnapshotWithCompression( 'plugins/foo/target/public/foo.chunk.1.js', diff --git a/packages/kbn-optimizer/src/limits.ts b/packages/kbn-optimizer/src/limits.ts index fcfd36664c1f4..292314a4608e4 100644 --- a/packages/kbn-optimizer/src/limits.ts +++ b/packages/kbn-optimizer/src/limits.ts @@ -7,12 +7,13 @@ */ import Fs from 'fs'; +import Path from 'path'; import dedent from 'dedent'; import Yaml from 'js-yaml'; -import { createFailError, ToolingLog } from '@kbn/dev-utils'; +import { createFailError, ToolingLog, CiStatsMetrics } from '@kbn/dev-utils'; -import { OptimizerConfig, getMetrics, Limits } from './optimizer'; +import { OptimizerConfig, Limits } from './optimizer'; const LIMITS_PATH = require.resolve('../limits.yml'); const DEFAULT_BUDGET = 15000; @@ -33,7 +34,7 @@ export function readLimits(): Limits { } export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerConfig) { - const limitBundleIds = Object.keys(config.limits.pageLoadAssetSize || {}); + const limitBundleIds = Object.keys(readLimits().pageLoadAssetSize || {}); const configBundleIds = config.bundles.map((b) => b.id); const missingBundleIds = diff(configBundleIds, limitBundleIds); @@ -75,15 +76,21 @@ interface UpdateBundleLimitsOptions { } export function updateBundleLimits({ log, config, dropMissing }: UpdateBundleLimitsOptions) { - const metrics = getMetrics(log, config); + const limits = readLimits(); + const metrics: CiStatsMetrics = config.bundles + .map((bundle) => + JSON.parse(Fs.readFileSync(Path.resolve(bundle.outputDir, 'metrics.json'), 'utf-8')) + ) + .flat() + .sort((a, b) => a.id.localeCompare(b.id)); const pageLoadAssetSize: NonNullable = dropMissing ? {} - : config.limits.pageLoadAssetSize ?? {}; + : limits.pageLoadAssetSize ?? {}; - for (const metric of metrics.sort((a, b) => a.id.localeCompare(b.id))) { + for (const metric of metrics) { if (metric.group === 'page load bundle size') { - const existingLimit = config.limits.pageLoadAssetSize?.[metric.id]; + const existingLimit = limits.pageLoadAssetSize?.[metric.id]; pageLoadAssetSize[metric.id] = existingLimit != null && existingLimit >= metric.value ? existingLimit diff --git a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts deleted file mode 100644 index e7059c4d6799c..0000000000000 --- a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts +++ /dev/null @@ -1,118 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Fs from 'fs'; -import Path from 'path'; - -import { ToolingLog, CiStatsMetrics } from '@kbn/dev-utils'; -import { OptimizerConfig } from './optimizer_config'; - -const flatten = (arr: Array): T[] => - arr.reduce((acc: T[], item) => acc.concat(item), []); - -interface Entry { - relPath: string; - stats: Fs.Stats; -} - -const IGNORED_EXTNAME = ['.map', '.br', '.gz']; - -const getFiles = (dir: string, parent?: string) => - flatten( - Fs.readdirSync(dir).map((name): Entry | Entry[] => { - const absPath = Path.join(dir, name); - const relPath = parent ? Path.join(parent, name) : name; - const stats = Fs.statSync(absPath); - - if (stats.isDirectory()) { - return getFiles(absPath, relPath); - } - - return { - relPath, - stats, - }; - }) - ).filter((file) => { - const filename = Path.basename(file.relPath); - if (filename.startsWith('.')) { - return false; - } - - const ext = Path.extname(filename); - if (IGNORED_EXTNAME.includes(ext)) { - return false; - } - - return true; - }); - -export function getMetrics(log: ToolingLog, config: OptimizerConfig) { - return flatten( - config.bundles.map((bundle) => { - // make the cache read from the cache file since it was likely updated by the worker - bundle.cache.refresh(); - - const outputFiles = getFiles(bundle.outputDir); - const entryName = `${bundle.id}.${bundle.type}.js`; - const entry = outputFiles.find((f) => f.relPath === entryName); - if (!entry) { - throw new Error( - `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` - ); - } - - const chunkPrefix = `${bundle.id}.chunk.`; - const asyncChunks = outputFiles.filter((f) => f.relPath.startsWith(chunkPrefix)); - const miscFiles = outputFiles.filter((f) => f !== entry && !asyncChunks.includes(f)); - - if (asyncChunks.length) { - log.verbose(bundle.id, 'async chunks', asyncChunks); - } - if (miscFiles.length) { - log.verbose(bundle.id, 'misc files', asyncChunks); - } - - const sumSize = (files: Entry[]) => files.reduce((acc: number, f) => acc + f.stats!.size, 0); - - const bundleMetrics: CiStatsMetrics = [ - { - group: `@kbn/optimizer bundle module count`, - id: bundle.id, - value: bundle.cache.getModuleCount() || 0, - }, - { - group: `page load bundle size`, - id: bundle.id, - value: entry.stats!.size, - limit: config.limits.pageLoadAssetSize?.[bundle.id], - limitConfigPath: `packages/kbn-optimizer/limits.yml`, - }, - { - group: `async chunks size`, - id: bundle.id, - value: sumSize(asyncChunks), - }, - { - group: `async chunk count`, - id: bundle.id, - value: asyncChunks.length, - }, - { - group: `miscellaneous assets size`, - id: bundle.id, - value: sumSize(miscFiles), - }, - ]; - - log.debug(bundle.id, 'metrics', bundleMetrics); - - return bundleMetrics; - }) - ); -} diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index d921d5e5cca31..e4cdddbf56dcb 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -48,7 +48,12 @@ it('returns a bundle for core and each plugin', () => { }, ], '/repo', - '/output' + '/output', + { + pageLoadAssetSize: { + box: 123, + }, + } ).map((b) => b.toSpec()) ).toMatchInlineSnapshot(` Array [ @@ -58,6 +63,7 @@ it('returns a bundle for core and each plugin', () => { "id": "foo", "manifestPath": /plugins/foo/kibana.json, "outputDir": /plugins/foo/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -70,6 +76,7 @@ it('returns a bundle for core and each plugin', () => { "id": "baz", "manifestPath": /plugins/baz/kibana.json, "outputDir": /plugins/baz/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -84,6 +91,7 @@ it('returns a bundle for core and each plugin', () => { "id": "box", "manifestPath": /x-pack/plugins/box/kibana.json, "outputDir": /x-pack/plugins/box/target/public, + "pageLoadAssetSizeLimit": 123, "publicDirNames": Array [ "public", ], diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index 76a0d51edac82..8134707561bc0 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -9,13 +9,15 @@ import Path from 'path'; import { Bundle } from '../common'; +import { Limits } from './optimizer_config'; import { KibanaPlatformPlugin } from './kibana_platform_plugins'; export function getPluginBundles( plugins: KibanaPlatformPlugin[], repoRoot: string, - outputRoot: string + outputRoot: string, + limits: Limits ) { const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; @@ -39,6 +41,7 @@ export function getPluginBundles( ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. \n` + ` * Licensed under the Elastic License 2.0; you may not use this file except in compliance with the Elastic License 2.0. */\n` : undefined, + pageLoadAssetSizeLimit: limits.pageLoadAssetSize?.[p.id], }) ); } diff --git a/packages/kbn-optimizer/src/optimizer/index.ts b/packages/kbn-optimizer/src/optimizer/index.ts index ced61463d5edd..28d206488b0a4 100644 --- a/packages/kbn-optimizer/src/optimizer/index.ts +++ b/packages/kbn-optimizer/src/optimizer/index.ts @@ -14,4 +14,3 @@ export * from './watch_bundles_for_changes'; export * from './run_workers'; export * from './bundle_cache'; export * from './handle_optimizer_completion'; -export * from './get_output_stats'; diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index 5677719628b6a..c60d6719cdea7 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -435,7 +435,6 @@ describe('OptimizerConfig::create()', () => { "cache": Symbol(parsed cache), "dist": Symbol(parsed dist), "inspectWorkers": Symbol(parsed inspect workers), - "limits": Symbol(limits), "maxWorkerCount": Symbol(parsed max worker count), "plugins": Symbol(new platform plugins), "profileWebpack": Symbol(parsed profile webpack), @@ -457,7 +456,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 21, + 22, ], "results": Array [ Object { @@ -480,7 +479,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 24, + 25, ], "results": Array [ Object { @@ -498,13 +497,14 @@ describe('OptimizerConfig::create()', () => { Symbol(new platform plugins), Symbol(parsed repo root), Symbol(parsed output root), + Symbol(limits), ], ], "instances": Array [ [Window], ], "invocationCallOrder": Array [ - 22, + 23, ], "results": Array [ Object { diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index b93d7a753c9ac..ed521d32a0a29 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -211,6 +211,7 @@ export class OptimizerConfig { } static create(inputOptions: Options) { + const limits = readLimits(); const options = OptimizerConfig.parseOptions(inputOptions); const plugins = findKibanaPlatformPlugins(options.pluginScanDirs, options.pluginPaths); const bundles = [ @@ -223,10 +224,11 @@ export class OptimizerConfig { sourceRoot: options.repoRoot, contextDir: Path.resolve(options.repoRoot, 'src/core'), outputDir: Path.resolve(options.outputRoot, 'src/core/target/public'), + pageLoadAssetSizeLimit: limits.pageLoadAssetSize?.core, }), ] : []), - ...getPluginBundles(plugins, options.repoRoot, options.outputRoot), + ...getPluginBundles(plugins, options.repoRoot, options.outputRoot, limits), ]; return new OptimizerConfig( @@ -239,8 +241,7 @@ export class OptimizerConfig { options.maxWorkerCount, options.dist, options.profileWebpack, - options.themeTags, - readLimits() + options.themeTags ); } @@ -254,8 +255,7 @@ export class OptimizerConfig { public readonly maxWorkerCount: number, public readonly dist: boolean, public readonly profileWebpack: boolean, - public readonly themeTags: ThemeTags, - public readonly limits: Limits + public readonly themeTags: ThemeTags ) {} getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig { diff --git a/packages/kbn-optimizer/src/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts deleted file mode 100644 index eeed2fb1b156c..0000000000000 --- a/packages/kbn-optimizer/src/report_optimizer_stats.ts +++ /dev/null @@ -1,46 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { materialize, mergeMap, dematerialize } from 'rxjs/operators'; -import { CiStatsReporter, ToolingLog } from '@kbn/dev-utils'; - -import { OptimizerUpdate$ } from './run_optimizer'; -import { OptimizerConfig, getMetrics } from './optimizer'; -import { pipeClosure } from './common'; - -export function reportOptimizerStats( - reporter: CiStatsReporter, - config: OptimizerConfig, - log: ToolingLog -) { - return pipeClosure((update$: OptimizerUpdate$) => - update$.pipe( - materialize(), - mergeMap(async (n) => { - if (n.kind === 'C') { - const metrics = getMetrics(log, config); - - await reporter.metrics(metrics); - - for (const metric of metrics) { - if (metric.limit != null && metric.value > metric.limit) { - const value = metric.value.toLocaleString(); - const limit = metric.limit.toLocaleString(); - log.warning( - `Metric [${metric.group}] for [${metric.id}] of [${value}] over the limit of [${limit}]` - ); - } - } - } - - return n; - }), - dematerialize() - ) - ); -} diff --git a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts new file mode 100644 index 0000000000000..909a97a3e11c7 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; + +import webpack from 'webpack'; +import { RawSource } from 'webpack-sources'; +import { CiStatsMetrics } from '@kbn/dev-utils'; + +import { Bundle } from '../common'; + +interface Asset { + name: string; + size: number; +} + +const IGNORED_EXTNAME = ['.map', '.br', '.gz']; + +export class BundleMetricsPlugin { + constructor(private readonly bundle: Bundle) {} + + public apply(compiler: webpack.Compiler) { + const { bundle } = this; + + compiler.hooks.emit.tap('BundleMetricsPlugin', (compilation) => { + const assets = Object.entries(compilation.assets) + .map( + ([name, source]: [string, any]): Asset => ({ + name, + size: source.size(), + }) + ) + .filter((asset) => { + const filename = Path.basename(asset.name); + if (filename.startsWith('.')) { + return false; + } + + const ext = Path.extname(filename); + if (IGNORED_EXTNAME.includes(ext)) { + return false; + } + + return true; + }); + + const entryName = `${bundle.id}.${bundle.type}.js`; + const entry = assets.find((a) => a.name === entryName); + if (!entry) { + throw new Error( + `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` + ); + } + + const chunkPrefix = `${bundle.id}.chunk.`; + const asyncChunks = assets.filter((a) => a.name.startsWith(chunkPrefix)); + const miscFiles = assets.filter((a) => a !== entry && !asyncChunks.includes(a)); + + const sumSize = (files: Asset[]) => files.reduce((acc: number, a) => acc + a.size, 0); + + const moduleCount = bundle.cache.getModuleCount(); + if (moduleCount === undefined) { + throw new Error(`moduleCount wasn't populated by PopulateBundleCachePlugin`); + } + + const bundleMetrics: CiStatsMetrics = [ + { + group: `@kbn/optimizer bundle module count`, + id: bundle.id, + value: moduleCount, + }, + { + group: `page load bundle size`, + id: bundle.id, + value: entry.size, + limit: bundle.pageLoadAssetSizeLimit, + limitConfigPath: `packages/kbn-optimizer/limits.yml`, + }, + { + group: `async chunks size`, + id: bundle.id, + value: sumSize(asyncChunks), + }, + { + group: `async chunk count`, + id: bundle.id, + value: asyncChunks.length, + }, + { + group: `miscellaneous assets size`, + id: bundle.id, + value: sumSize(miscFiles), + }, + ]; + + const metricsSource = new RawSource(JSON.stringify(bundleMetrics, null, 2)); + + // see https://github.com/jantimon/html-webpack-plugin/blob/33d69f49e6e9787796402715d1b9cd59f80b628f/index.js#L266 + // @ts-expect-error undocumented, used to add assets to the output + compilation.emitAsset('metrics.json', metricsSource); + }); + } +} diff --git a/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts b/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts new file mode 100644 index 0000000000000..c964219e1fed6 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Fs from 'fs'; +import Path from 'path'; + +import webpack from 'webpack'; + +import { Bundle } from '../common'; + +export class EmitStatsPlugin { + constructor(private readonly bundle: Bundle) {} + + public apply(compiler: webpack.Compiler) { + compiler.hooks.done.tap( + { + name: 'EmitStatsPlugin', + // run at the very end, ensure that it's after clean-webpack-plugin + stage: 10, + }, + (stats) => { + Fs.writeFileSync( + Path.resolve(this.bundle.outputDir, 'stats.json'), + JSON.stringify(stats.toJson()) + ); + } + ); + } +} diff --git a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts new file mode 100644 index 0000000000000..6d296b9be089c --- /dev/null +++ b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import webpack from 'webpack'; + +import Path from 'path'; +import { inspect } from 'util'; + +import { Bundle, WorkerConfig, ascending, parseFilePath } from '../common'; +import { BundleRefModule } from './bundle_ref_module'; +import { + isExternalModule, + isNormalModule, + isIgnoredModule, + isConcatenatedModule, + getModulePath, +} from './webpack_helpers'; + +/** + * sass-loader creates about a 40% overhead on the overall optimizer runtime, and + * so this constant is used to indicate to assignBundlesToWorkers() that there is + * extra work done in a bundle that has a lot of scss imports. The value is + * arbitrary and just intended to weigh the bundles so that they are distributed + * across mulitple workers on machines with lots of cores. + */ +const EXTRA_SCSS_WORK_UNITS = 100; + +export class PopulateBundleCachePlugin { + constructor(private readonly workerConfig: WorkerConfig, private readonly bundle: Bundle) {} + + public apply(compiler: webpack.Compiler) { + const { bundle, workerConfig } = this; + + compiler.hooks.emit.tap( + { + name: 'PopulateBundleCachePlugin', + before: ['BundleMetricsPlugin'], + }, + (compilation) => { + const bundleRefExportIds: string[] = []; + const referencedFiles = new Set(); + let moduleCount = 0; + let workUnits = compilation.fileDependencies.size; + + if (bundle.manifestPath) { + referencedFiles.add(bundle.manifestPath); + } + + for (const module of compilation.modules) { + if (isNormalModule(module)) { + moduleCount += 1; + const path = getModulePath(module); + const parsedPath = parseFilePath(path); + + if (!parsedPath.dirs.includes('node_modules')) { + referencedFiles.add(path); + + if (path.endsWith('.scss')) { + workUnits += EXTRA_SCSS_WORK_UNITS; + + for (const depPath of module.buildInfo.fileDependencies) { + referencedFiles.add(depPath); + } + } + + continue; + } + + const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); + const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); + referencedFiles.add( + Path.join( + parsedPath.root, + ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), + 'package.json' + ) + ); + continue; + } + + if (module instanceof BundleRefModule) { + bundleRefExportIds.push(module.ref.exportId); + continue; + } + + if (isConcatenatedModule(module)) { + moduleCount += module.modules.length; + continue; + } + + if (isExternalModule(module) || isIgnoredModule(module)) { + continue; + } + + throw new Error(`Unexpected module type: ${inspect(module)}`); + } + + const files = Array.from(referencedFiles).sort(ascending((p) => p)); + const mtimes = new Map( + files.map((path): [string, number | undefined] => { + try { + return [path, compiler.inputFileSystem.statSync(path)?.mtimeMs]; + } catch (error) { + if (error?.code === 'ENOENT') { + return [path, undefined]; + } + + throw error; + } + }) + ); + + bundle.cache.set({ + bundleRefExportIds: bundleRefExportIds.sort(ascending((p) => p)), + optimizerCacheKey: workerConfig.optimizerCacheKey, + cacheKey: bundle.createCacheKey(files, mtimes), + moduleCount, + workUnits, + files, + }); + + // write the cache to the compilation so that it isn't cleaned by clean-webpack-plugin + bundle.cache.writeWebpackAsset(compilation); + } + ); + } +} diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index 61f9c243a4def..4f5bb23c3550d 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -8,46 +8,16 @@ import 'source-map-support/register'; -import Fs from 'fs'; -import Path from 'path'; -import { inspect } from 'util'; - import webpack, { Stats } from 'webpack'; import * as Rx from 'rxjs'; import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators'; -import { - CompilerMsgs, - CompilerMsg, - maybeMap, - Bundle, - WorkerConfig, - ascending, - parseFilePath, - BundleRefs, -} from '../common'; -import { BundleRefModule } from './bundle_ref_module'; +import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig, BundleRefs } from '../common'; import { getWebpackConfig } from './webpack.config'; import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; -import { - isExternalModule, - isNormalModule, - isIgnoredModule, - isConcatenatedModule, - getModulePath, -} from './webpack_helpers'; const PLUGIN_NAME = '@kbn/optimizer'; -/** - * sass-loader creates about a 40% overhead on the overall optimizer runtime, and - * so this constant is used to indicate to assignBundlesToWorkers() that there is - * extra work done in a bundle that has a lot of scss imports. The value is - * arbitrary and just intended to weigh the bundles so that they are distributed - * across mulitple workers on machines with lots of cores. - */ -const EXTRA_SCSS_WORK_UNITS = 100; - /** * Create an Observable for a specific child compiler + bundle */ @@ -80,13 +50,6 @@ const observeCompiler = ( return undefined; } - if (workerConfig.profileWebpack) { - Fs.writeFileSync( - Path.resolve(bundle.outputDir, 'stats.json'), - JSON.stringify(stats.toJson()) - ); - } - if (!workerConfig.watch) { process.nextTick(() => done$.next()); } @@ -97,88 +60,11 @@ const observeCompiler = ( }); } - const bundleRefExportIds: string[] = []; - const referencedFiles = new Set(); - let moduleCount = 0; - let workUnits = stats.compilation.fileDependencies.size; - - if (bundle.manifestPath) { - referencedFiles.add(bundle.manifestPath); - } - - for (const module of stats.compilation.modules) { - if (isNormalModule(module)) { - moduleCount += 1; - const path = getModulePath(module); - const parsedPath = parseFilePath(path); - - if (!parsedPath.dirs.includes('node_modules')) { - referencedFiles.add(path); - - if (path.endsWith('.scss')) { - workUnits += EXTRA_SCSS_WORK_UNITS; - - for (const depPath of module.buildInfo.fileDependencies) { - referencedFiles.add(depPath); - } - } - - continue; - } - - const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); - const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); - referencedFiles.add( - Path.join( - parsedPath.root, - ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), - 'package.json' - ) - ); - continue; - } - - if (module instanceof BundleRefModule) { - bundleRefExportIds.push(module.ref.exportId); - continue; - } - - if (isConcatenatedModule(module)) { - moduleCount += module.modules.length; - continue; - } - - if (isExternalModule(module) || isIgnoredModule(module)) { - continue; - } - - throw new Error(`Unexpected module type: ${inspect(module)}`); + const moduleCount = bundle.cache.getModuleCount(); + if (moduleCount === undefined) { + throw new Error(`moduleCount wasn't populated by PopulateBundleCachePlugin`); } - const files = Array.from(referencedFiles).sort(ascending((p) => p)); - const mtimes = new Map( - files.map((path): [string, number | undefined] => { - try { - return [path, compiler.inputFileSystem.statSync(path)?.mtimeMs]; - } catch (error) { - if (error?.code === 'ENOENT') { - return [path, undefined]; - } - - throw error; - } - }) - ); - - bundle.cache.set({ - bundleRefExportIds: bundleRefExportIds.sort(ascending((p) => p)), - optimizerCacheKey: workerConfig.optimizerCacheKey, - cacheKey: bundle.createCacheKey(files, mtimes), - moduleCount, - workUnits, - files, - }); - return compilerMsgs.compilerSuccess({ moduleCount, }); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 331fbde6ea0ba..c4beb959284cc 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -19,6 +19,9 @@ import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { Bundle, BundleRefs, WorkerConfig } from '../common'; import { BundleRefsPlugin } from './bundle_refs_plugin'; +import { BundleMetricsPlugin } from './bundle_metrics_plugin'; +import { EmitStatsPlugin } from './emit_stats_plugin'; +import { PopulateBundleCachePlugin } from './populate_bundle_cache_plugin'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); @@ -67,6 +70,9 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: plugins: [ new CleanWebpackPlugin(), new BundleRefsPlugin(bundle, bundleRefs), + new PopulateBundleCachePlugin(worker, bundle), + new BundleMetricsPlugin(bundle), + ...(worker.profileWebpack ? [new EmitStatsPlugin(bundle)] : []), ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ], diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts index 559d9da35c320..9723c0107cf8e 100644 --- a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -74,13 +74,14 @@ it('builds a generated plugin into a viable archive', async () => { await extract(PLUGIN_ARCHIVE, { dir: TMP_DIR }); - const files = await globby(['**/*'], { cwd: TMP_DIR }); + const files = await globby(['**/*'], { cwd: TMP_DIR, dot: true }); files.sort((a, b) => a.localeCompare(b)); expect(files).toMatchInlineSnapshot(` Array [ "kibana/fooTestPlugin/common/index.js", "kibana/fooTestPlugin/kibana.json", + "kibana/fooTestPlugin/node_modules/.yarn-integrity", "kibana/fooTestPlugin/package.json", "kibana/fooTestPlugin/server/index.js", "kibana/fooTestPlugin/server/plugin.js", diff --git a/packages/kbn-plugin-helpers/src/tasks/optimize.ts b/packages/kbn-plugin-helpers/src/tasks/optimize.ts index 0f0ac93086c9e..2478947e79f18 100644 --- a/packages/kbn-plugin-helpers/src/tasks/optimize.ts +++ b/packages/kbn-plugin-helpers/src/tasks/optimize.ts @@ -34,9 +34,15 @@ export async function optimize({ log, plugin, sourceDir, buildDir }: BuildContex pluginScanDirs: [], }); + const target = Path.resolve(sourceDir, 'target'); + await runOptimizer(config).pipe(logOptimizerState(log, config)).toPromise(); + // clean up unnecessary files + Fs.unlinkSync(Path.resolve(target, 'public/metrics.json')); + Fs.unlinkSync(Path.resolve(target, 'public/.kbn-optimizer-cache')); + // move target into buildDir - await asyncRename(Path.resolve(sourceDir, 'target'), Path.resolve(buildDir, 'target')); + await asyncRename(target, Path.resolve(buildDir, 'target')); log.indent(-2); } diff --git a/scripts/ship_ci_stats.js b/scripts/ship_ci_stats.js new file mode 100644 index 0000000000000..5aed9fc446240 --- /dev/null +++ b/scripts/ship_ci_stats.js @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env/no_transpilation'); +require('@kbn/dev-utils').shipCiStatsCli(); diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index 91fad2ca52617..d2d2d3275270b 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.ts +++ b/src/dev/build/tasks/build_kibana_platform_plugins.ts @@ -6,20 +6,18 @@ * Side Public License, v 1. */ +import Path from 'path'; + import { REPO_ROOT } from '@kbn/utils'; -import { CiStatsReporter } from '@kbn/dev-utils'; -import { - runOptimizer, - OptimizerConfig, - logOptimizerState, - reportOptimizerStats, -} from '@kbn/optimizer'; +import { lastValueFrom } from '@kbn/std'; +import { CiStatsMetrics } from '@kbn/dev-utils'; +import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; -import { Task } from '../lib'; +import { Task, deleteAll, write, read } from '../lib'; export const BuildKibanaPlatformPlugins: Task = { description: 'Building distributable versions of Kibana platform plugins', - async run(_, log, build) { + async run(buildConfig, log, build) { const config = OptimizerConfig.create({ repoRoot: REPO_ROOT, outputRoot: build.resolvePath(), @@ -31,12 +29,27 @@ export const BuildKibanaPlatformPlugins: Task = { includeCoreBundle: true, }); - const reporter = CiStatsReporter.fromEnv(log); + await lastValueFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); + + const combinedMetrics: CiStatsMetrics = []; + const metricFilePaths: string[] = []; + for (const bundle of config.bundles) { + const path = Path.resolve(bundle.outputDir, 'metrics.json'); + const metrics: CiStatsMetrics = JSON.parse(await read(path)); + combinedMetrics.push(...metrics); + metricFilePaths.push(path); + } + + // write combined metrics to target + await write( + buildConfig.resolveFromTarget('optimizer_bundle_metrics.json'), + JSON.stringify(combinedMetrics, null, 2) + ); - await runOptimizer(config) - .pipe(reportOptimizerStats(reporter, config, log), logOptimizerState(log, config)) - .toPromise(); + // delete all metric files + await deleteAll(metricFilePaths, log); + // delete all bundle cache files await Promise.all(config.bundles.map((b) => b.cache.clear())); }, }; diff --git a/test/scripts/jenkins_baseline.sh b/test/scripts/jenkins_baseline.sh index e679ac7f31bd1..60926238576c7 100755 --- a/test/scripts/jenkins_baseline.sh +++ b/test/scripts/jenkins_baseline.sh @@ -5,6 +5,10 @@ source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh" echo " -> building and extracting OSS Kibana distributable for use in functional tests" node scripts/build --debug --oss + +echo " -> shipping metrics from build to ci-stats" +node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$PARENT_DIR/install/kibana" mkdir -p "$installDir" diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 6184708ea3fc6..5819a3ce6765e 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -17,6 +17,9 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting OSS Kibana distributable for use in functional tests" node scripts/build --debug --oss + echo " -> shipping metrics from build to ci-stats" + node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + mkdir -p "$WORKSPACE/kibana-build-oss" cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/ fi diff --git a/test/scripts/jenkins_xpack_baseline.sh b/test/scripts/jenkins_xpack_baseline.sh index 7577b6927d166..aaacdd4ea3aae 100755 --- a/test/scripts/jenkins_xpack_baseline.sh +++ b/test/scripts/jenkins_xpack_baseline.sh @@ -6,6 +6,10 @@ source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh" echo " -> building and extracting default Kibana distributable" cd "$KIBANA_DIR" node scripts/build --debug --no-oss + +echo " -> shipping metrics from build to ci-stats" +node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index a9e603f63bd42..36865ce7c4967 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -32,6 +32,10 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting default Kibana distributable for use in functional tests" cd "$KIBANA_DIR" node scripts/build --debug --no-oss + + echo " -> shipping metrics from build to ci-stats" + node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" diff --git a/yarn.lock b/yarn.lock index 6df258e9715b7..ec6cf338a43da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6803,7 +6803,7 @@ dependencies: "@types/webpack" "*" -"@types/webpack-sources@*": +"@types/webpack-sources@*", "@types/webpack-sources@^0.1.4": version "0.1.5" resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== From 9684661da4e674f77a00bd59b9e5aa3897d418eb Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Mon, 8 Feb 2021 13:28:18 -0600 Subject: [PATCH 020/100] [Metrics UI] Add ability to filter anomaly detection datafeed (#89721) * Add null check for empty process data * Add Ability to filter datafeed for ml jobs * Merge user-defined query with default query Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/containers/ml/infra_ml_module.tsx | 9 ++- .../containers/ml/infra_ml_module_types.ts | 3 +- .../metrics_hosts/module_descriptor.ts | 18 ++++- .../modules/metrics_k8s/module_descriptor.ts | 18 ++++- .../ml/anomaly_detection/job_setup_screen.tsx | 65 +++++++++++++++++-- 5 files changed, 98 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx index a94f2dd57c482..b55ae65e58e91 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx @@ -6,7 +6,6 @@ */ import { useCallback, useMemo } from 'react'; -import { DatasetFilter } from '../../../common/infra_ml'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { useTrackedPromise } from '../../utils/use_tracked_promise'; import { useModuleStatus } from './infra_ml_module_status'; @@ -52,7 +51,7 @@ export const useInfraMLModule = ({ selectedIndices: string[], start: number | undefined, end: number | undefined, - datasetFilter: DatasetFilter, + filter: string, partitionField?: string ) => { dispatchModuleStatus({ type: 'startedSetup' }); @@ -60,7 +59,7 @@ export const useInfraMLModule = ({ { start, end, - datasetFilter, + filter, moduleSourceConfiguration: { indices: selectedIndices, sourceId, @@ -114,13 +113,13 @@ export const useInfraMLModule = ({ selectedIndices: string[], start: number | undefined, end: number | undefined, - datasetFilter: DatasetFilter, + filter: string, partitionField?: string ) => { dispatchModuleStatus({ type: 'startedSetup' }); cleanUpModule() .then(() => { - setUpModule(selectedIndices, start, end, datasetFilter, partitionField); + setUpModule(selectedIndices, start, end, filter, partitionField); }) .catch(() => { dispatchModuleStatus({ type: 'failedSetup' }); diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts index e681290570b8c..5a5272f783053 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts @@ -10,7 +10,6 @@ import { ValidateLogEntryDatasetsResponsePayload, ValidationIndicesResponsePayload, } from '../../../common/http_api/log_analysis'; -import { DatasetFilter } from '../../../common/infra_ml'; import { DeleteJobsResponsePayload } from './api/ml_cleanup'; import { FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api'; import { GetMlModuleResponsePayload } from './api/ml_get_module'; @@ -21,7 +20,7 @@ export { JobModelSizeStats, JobSummary } from './api/ml_get_jobs_summary_api'; export interface SetUpModuleArgs { start?: number | undefined; end?: number | undefined; - datasetFilter?: DatasetFilter; + filter?: any; moduleSourceConfiguration: ModuleSourceConfiguration; partitionField?: string; } diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts index b8d09fdb5e325..a7ab948d052aa 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts @@ -67,6 +67,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const { start, end, + filter, moduleSourceConfiguration: { spaceId, sourceId, indices, timestampField }, partitionField, } = setUpModuleArgs; @@ -107,10 +108,23 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const datafeedOverrides = jobIds.map((id) => { const { datafeed: defaultDatafeedConfig } = getDefaultJobConfigs(id); + const config = { ...defaultDatafeedConfig }; + + if (filter) { + const query = JSON.parse(filter); + + config.query.bool = { + ...config.query.bool, + ...query.bool, + }; + } if (!partitionField || id === 'hosts_memory_usage') { // Since the host memory usage doesn't have custom aggs, we don't need to do anything to add a partition field - return defaultDatafeedConfig; + return { + ...config, + job_id: id, + }; } // If we have a partition field, we need to change the aggregation to do a terms agg at the top level @@ -126,7 +140,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) }; return { - ...defaultDatafeedConfig, + ...config, job_id: id, aggregations, }; diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts index fe92b290dfde3..4c5eb5fd4bf23 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts @@ -68,6 +68,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const { start, end, + filter, moduleSourceConfiguration: { spaceId, sourceId, indices, timestampField }, partitionField, } = setUpModuleArgs; @@ -107,10 +108,23 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const datafeedOverrides = jobIds.map((id) => { const { datafeed: defaultDatafeedConfig } = getDefaultJobConfigs(id); + const config = { ...defaultDatafeedConfig }; + + if (filter) { + const query = JSON.parse(filter); + + config.query.bool = { + ...config.query.bool, + ...query.bool, + }; + } if (!partitionField || id === 'k8s_memory_usage') { // Since the host memory usage doesn't have custom aggs, we don't need to do anything to add a partition field - return defaultDatafeedConfig; + return { + ...config, + job_id: id, + }; } // Because the ML K8s jobs ship with a default partition field of {kubernetes.namespace}, ignore that agg and wrap it in our own agg. @@ -131,7 +145,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) }; return { - ...defaultDatafeedConfig, + ...config, job_id: id, aggregations, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx index 3236cbc59a07b..894f76318bcfe 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { debounce } from 'lodash'; import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { EuiForm, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import { EuiText, EuiSpacer } from '@elastic/eui'; @@ -22,6 +22,8 @@ import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modul import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module'; import { FixedDatePicker } from '../../../../../../components/fixed_datepicker'; import { DEFAULT_K8S_PARTITION_FIELD } from '../../../../../../containers/ml/modules/metrics_k8s/module_descriptor'; +import { MetricsExplorerKueryBar } from '../../../../metrics_explorer/components/kuery_bar'; +import { convertKueryToElasticSearchQuery } from '../../../../../../utils/kuery'; interface Props { jobType: 'hosts' | 'kubernetes'; @@ -36,6 +38,8 @@ export const JobSetupScreen = (props: Props) => { const [partitionField, setPartitionField] = useState(null); const h = useMetricHostsModuleContext(); const k = useMetricK8sModuleContext(); + const [filter, setFilter] = useState(''); + const [filterQuery, setFilterQuery] = useState(''); const { createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', type: 'metrics', @@ -89,7 +93,7 @@ export const JobSetupScreen = (props: Props) => { indicies, moment(startDate).toDate().getTime(), undefined, - { type: 'includeAll' }, + filterQuery, partitionField ? partitionField[0] : undefined ); } else { @@ -97,11 +101,30 @@ export const JobSetupScreen = (props: Props) => { indicies, moment(startDate).toDate().getTime(), undefined, - { type: 'includeAll' }, + filterQuery, partitionField ? partitionField[0] : undefined ); } - }, [cleanUpAndSetUpModule, setUpModule, hasSummaries, indicies, partitionField, startDate]); + }, [ + cleanUpAndSetUpModule, + filterQuery, + setUpModule, + hasSummaries, + indicies, + partitionField, + startDate, + ]); + + const onFilterChange = useCallback( + (f: string) => { + setFilter(f || ''); + setFilterQuery(convertKueryToElasticSearchQuery(f, derivedIndexPattern) || ''); + }, + [derivedIndexPattern] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnFilterChange = useCallback(debounce(onFilterChange, 500), [onFilterChange]); const onPartitionFieldChange = useCallback((value: Array<{ label: string }>) => { setPartitionField(value.map((v) => v.label)); @@ -250,6 +273,40 @@ export const JobSetupScreen = (props: Props) => { />
+ + + + + } + description={ + + } + > + + } + > + + + )} From 3722bea42f03fc7d2799d88fed6bb1aaba945055 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 8 Feb 2021 12:53:54 -0700 Subject: [PATCH 021/100] [Maps] clamp MVT too many features polygon to tile boundary (#90444) * [Maps] clamp MVT too many features polygon to tile boundary * add mapbox_styles to index.js Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/server/mvt/get_tile.ts | 84 ++-- x-pack/test/functional/apps/maps/index.js | 1 + x-pack/test/functional/apps/maps/joins.js | 34 -- .../functional/apps/maps/mapbox_styles.js | 358 +++++++++++------- 4 files changed, 242 insertions(+), 235 deletions(-) diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 3116838d26fb5..50c2014275a0f 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -25,7 +25,7 @@ import { import { convertRegularRespToGeoJson, hitsToGeoJson } from '../../common/elasticsearch_util'; import { flattenHit } from './util'; -import { ESBounds, tile2lat, tile2long, tileToESBbox } from '../../common/geo_tile_utils'; +import { ESBounds, tileToESBbox } from '../../common/geo_tile_utils'; import { getCentroidFeatures } from '../../common/get_centroid_features'; export async function getGridTile({ @@ -53,35 +53,14 @@ export async function getGridTile({ geoFieldType: ES_GEO_FIELD_TYPE; searchSessionId?: string; }): Promise { - const esBbox: ESBounds = tileToESBbox(x, y, z); try { - let bboxFilter; - if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - bboxFilter = { - geo_bounding_box: { - [geometryFieldName]: esBbox, - }, - }; - } else if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { - const geojsonPolygon = tileToGeoJsonPolygon(x, y, z); - bboxFilter = { - geo_shape: { - [geometryFieldName]: { - shape: geojsonPolygon, - relation: 'INTERSECTS', - }, - }, - }; - } else { - throw new Error(`${geoFieldType} is not valid geo field-type`); - } - requestBody.query.bool.filter.push(bboxFilter); - + const tileBounds: ESBounds = tileToESBbox(x, y, z); + requestBody.query.bool.filter.push(getTileSpatialFilter(geometryFieldName, tileBounds)); requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.precision = Math.min( z + SUPER_FINE_ZOOM_DELTA, MAX_ZOOM ); - requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = esBbox; + requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = tileBounds; const response = await context .search!.search( @@ -134,14 +113,9 @@ export async function getTile({ }): Promise { let features: Feature[]; try { - requestBody.query.bool.filter.push({ - geo_shape: { - [geometryFieldName]: { - shape: tileToGeoJsonPolygon(x, y, z), - relation: 'INTERSECTS', - }, - }, - }); + requestBody.query.bool.filter.push( + getTileSpatialFilter(geometryFieldName, tileToESBbox(x, y, z)) + ); const searchOptions = { sessionId: searchSessionId, @@ -193,7 +167,8 @@ export async function getTile({ [KBN_TOO_MANY_FEATURES_PROPERTY]: true, }, geometry: esBboxToGeoJsonPolygon( - bboxResponse.rawResponse.aggregations.data_bounds.bounds + bboxResponse.rawResponse.aggregations.data_bounds.bounds, + tileToESBbox(x, y, z) ), }, ]; @@ -244,32 +219,31 @@ export async function getTile({ } } -function tileToGeoJsonPolygon(x: number, y: number, z: number): Polygon { - const wLon = tile2long(x, z); - const sLat = tile2lat(y + 1, z); - const eLon = tile2long(x + 1, z); - const nLat = tile2lat(y, z); - +function getTileSpatialFilter(geometryFieldName: string, tileBounds: ESBounds): unknown { return { - type: 'Polygon', - coordinates: [ - [ - [wLon, sLat], - [wLon, nLat], - [eLon, nLat], - [eLon, sLat], - [wLon, sLat], - ], - ], + geo_shape: { + [geometryFieldName]: { + shape: { + type: 'envelope', + // upper left and lower right points of the shape to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]] + coordinates: [ + [tileBounds.top_left.lon, tileBounds.top_left.lat], + [tileBounds.bottom_right.lon, tileBounds.bottom_right.lat], + ], + }, + relation: 'INTERSECTS', + }, + }, }; } -function esBboxToGeoJsonPolygon(esBounds: ESBounds): Polygon { - let minLon = esBounds.top_left.lon; - const maxLon = esBounds.bottom_right.lon; +function esBboxToGeoJsonPolygon(esBounds: ESBounds, tileBounds: ESBounds): Polygon { + // Intersecting geo_shapes may push bounding box outside of tile so need to clamp to tile bounds. + let minLon = Math.max(esBounds.top_left.lon, tileBounds.top_left.lon); + const maxLon = Math.min(esBounds.bottom_right.lon, tileBounds.bottom_right.lon); minLon = minLon > maxLon ? minLon - 360 : minLon; // fixes an ES bbox to straddle dateline - const minLat = esBounds.bottom_right.lat; - const maxLat = esBounds.top_left.lat; + const minLat = Math.max(esBounds.bottom_right.lat, tileBounds.bottom_right.lat); + const maxLat = Math.min(esBounds.top_left.lat, tileBounds.top_left.lat); return { type: 'Polygon', diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index d76afb7ebdc24..dd20ed58afbc6 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -47,6 +47,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./es_geo_grid_source')); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); + loadTestFile(require.resolve('./mapbox_styles')); loadTestFile(require.resolve('./mvt_scaling')); loadTestFile(require.resolve('./mvt_super_fine')); loadTestFile(require.resolve('./add_layer_panel')); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 094f5335cd05f..49717016f9c60 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -7,8 +7,6 @@ import expect from '@kbn/expect'; -import { MAPBOX_STYLES } from './mapbox_styles'; - const JOIN_PROPERTY_NAME = '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'; const EXPECTED_JOIN_VALUES = { alpha: 10, @@ -18,10 +16,6 @@ const EXPECTED_JOIN_VALUES = { }; const VECTOR_SOURCE_ID = 'n1t6f'; -const CIRCLE_STYLE_LAYER_INDEX = 0; -const FILL_STYLE_LAYER_INDEX = 2; -const LINE_STYLE_LAYER_INDEX = 3; -const TOO_MANY_FEATURES_LAYER_INDEX = 4; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); @@ -95,34 +89,6 @@ export default function ({ getPageObjects, getService }) { }); }); - it('should style fills, points, lines, and bounding-boxes independently', async () => { - const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - const layersForVectorSource = mapboxStyle.layers.filter((mbLayer) => { - return mbLayer.id.startsWith(VECTOR_SOURCE_ID); - }); - - //circle layer for points - expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.POINT_LAYER); - - //fill layer - expect(layersForVectorSource[FILL_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.FILL_LAYER); - - //line layer for borders - expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.LINE_LAYER); - - //Too many features layer (this is a static style config) - expect(layersForVectorSource[TOO_MANY_FEATURES_LAYER_INDEX]).to.eql({ - id: 'n1t6f_toomanyfeatures', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: ['==', ['get', '__kbn_too_many_features__'], true], - layout: { visibility: 'visible' }, - paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, - }); - }); - it('should flag only the joined features as visible', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index d4496f13b8bef..b483b95e0ca1f 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -5,176 +5,242 @@ * 2.0. */ -export const MAPBOX_STYLES = { - POINT_LAYER: { - id: 'n1t6f_circle', - type: 'circle', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], - ], - ], - layout: { visibility: 'visible' }, - paint: { - 'circle-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['maps']); + const inspector = getService('inspector'); + const security = getService('security'); + + describe('mapbox styles', () => { + let mapboxStyle; + before(async () => { + await security.testUser.setRoles( + ['global_maps_all', 'geoshape_data_reader', 'meta_for_geoshape_data_reader'], + false + ); + await PageObjects.maps.loadSavedMap('join example'); + mapboxStyle = await PageObjects.maps.getMapboxStyle(); + }); + + after(async () => { + await inspector.close(); + await security.testUser.restoreDefaults(); + }); + + it('should style circle layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_circle'; + }); + expect(layer).to.eql({ + id: 'n1t6f_circle', + type: 'circle', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'case', - [ - '==', - ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], - null, - ], - 2, + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], + ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], + ], + ], + layout: { visibility: 'visible' }, + paint: { + 'circle-color': [ + 'interpolate', + ['linear'], [ - 'max', + 'coalesce', [ - 'min', + 'case', [ - 'to-number', + '==', [ 'feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', + ], + ], + 12, + ], + 3, ], - 12, ], - 3, + 2, ], + 2, + 'rgba(0,0,0,0)', + 3, + '#ecf1f7', + 4.125, + '#d9e3ef', + 5.25, + '#c5d5e7', + 6.375, + '#b2c7df', + 7.5, + '#9eb9d8', + 8.625, + '#8bacd0', + 9.75, + '#769fc8', + 10.875, + '#6092c0', ], - 2, - ], - 2, - 'rgba(0,0,0,0)', - 3, - '#ecf1f7', - 4.125, - '#d9e3ef', - 5.25, - '#c5d5e7', - 6.375, - '#b2c7df', - 7.5, - '#9eb9d8', - 8.625, - '#8bacd0', - 9.75, - '#769fc8', - 10.875, - '#6092c0', - ], - 'circle-opacity': 0.75, - 'circle-stroke-color': '#41937c', - 'circle-stroke-opacity': 0.75, - 'circle-stroke-width': 1, - 'circle-radius': 10, - }, - }, - FILL_LAYER: { - id: 'n1t6f_fill', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], - ], - ], - layout: { visibility: 'visible' }, - paint: { - 'fill-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', + 'circle-opacity': 0.75, + 'circle-stroke-color': '#41937c', + 'circle-stroke-opacity': 0.75, + 'circle-stroke-width': 1, + 'circle-radius': 10, + }, + }); + }); + + it('should style fill layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_fill'; + }); + expect(layer).to.eql({ + id: 'n1t6f_fill', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'case', + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], [ - '==', - ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], - null, + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], ], - 2, + ], + ], + layout: { visibility: 'visible' }, + paint: { + 'fill-color': [ + 'interpolate', + ['linear'], [ - 'max', + 'coalesce', [ - 'min', + 'case', [ - 'to-number', + '==', [ 'feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', + ], + ], + 12, + ], + 3, ], - 12, ], - 3, + 2, + ], + 2, + 'rgba(0,0,0,0)', + 3, + '#ecf1f7', + 4.125, + '#d9e3ef', + 5.25, + '#c5d5e7', + 6.375, + '#b2c7df', + 7.5, + '#9eb9d8', + 8.625, + '#8bacd0', + 9.75, + '#769fc8', + 10.875, + '#6092c0', + ], + 'fill-opacity': 0.75, + }, + }); + }); + + it('should style fill layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_line'; + }); + expect(layer).to.eql({ + id: 'n1t6f_line', + type: 'line', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], + [ + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], + [ + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], + ['==', ['geometry-type'], 'LineString'], + ['==', ['geometry-type'], 'MultiLineString'], ], ], - 2, - ], - 2, - 'rgba(0,0,0,0)', - 3, - '#ecf1f7', - 4.125, - '#d9e3ef', - 5.25, - '#c5d5e7', - 6.375, - '#b2c7df', - 7.5, - '#9eb9d8', - 8.625, - '#8bacd0', - 9.75, - '#769fc8', - 10.875, - '#6092c0', - ], - 'fill-opacity': 0.75, - }, - }, - LINE_LAYER: { - id: 'n1t6f_line', - type: 'line', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - [ - 'any', - ['==', ['geometry-type'], 'Polygon'], - ['==', ['geometry-type'], 'MultiPolygon'], - ['==', ['geometry-type'], 'LineString'], - ['==', ['geometry-type'], 'MultiLineString'], ], - ], - ], - layout: { visibility: 'visible' }, - paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, - }, -}; + layout: { visibility: 'visible' }, + paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, + }); + }); + + it('should style incomplete data layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_toomanyfeatures'; + }); + expect(layer).to.eql({ + id: 'n1t6f_toomanyfeatures', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: ['==', ['get', '__kbn_too_many_features__'], true], + layout: { visibility: 'visible' }, + paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, + }); + }); + }); +} From a1a2536b5bb624d9dce989389319d7d527377d79 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 8 Feb 2021 21:26:57 +0100 Subject: [PATCH 022/100] [Uptime] Waterfall filters (#89185) * WIP * Use multi canvas solution * type * fix test * adde unit tests * reduce item to 150 * update margins * use constant * update z-index * added key * wip * wip * wip filters * reorgnaise components * fix issue * update filter * only highlight button * water fall test * styling * fix styling * test * fix types * update test * update ari hidden * added click telemetry for waterfall filters * added input click telemetry * update filter behaviour * fixed typo * fix type * fix styling * persist original resource number in waterfall sidebar when showing only highlighted resources * update waterfall filter collapse checkbox content * update use_bar_charts to work with filtered data * update network request total label to include filtered requests * adjust telemetry * add accessible text * add waterfall chart view telemetry * updated mime type filter label translations * adjust total network requests to use FormattedMessage * adjust translations and tests * use FormattedMessage in NetworkRequestsTotal * ensure sidebar persists when 0 resources match filter * use destructuring in waterfall sidebar item * reset collapse requests checkbox when filters are removed * update license headers Co-authored-by: Dominique Clarke Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/observability/public/index.ts | 1 + .../waterfall/data_formatting.test.ts | 330 +++++++++++------- .../step_detail/waterfall/data_formatting.ts | 65 +++- .../synthetics/step_detail/waterfall/types.ts | 43 +-- .../waterfall_chart_wrapper.test.tsx | 248 +++++++++++++ .../waterfall/waterfall_chart_wrapper.tsx | 102 +++--- .../waterfall/waterfall_filter.test.tsx | 155 ++++++++ .../waterfall/waterfall_filter.tsx | 188 ++++++++++ .../waterfall/waterfall_sidebar_item.tsx | 56 +++ .../waterfalll_sidebar_item.test.tsx | 51 +++ .../waterfall/components/constants.ts | 2 + .../components/middle_truncated_text.test.tsx | 12 +- .../components/middle_truncated_text.tsx | 9 +- .../network_requests_total.test.tsx | 51 ++- .../components/network_requests_total.tsx | 45 ++- .../waterfall/components/sidebar.tsx | 17 +- .../synthetics/waterfall/components/styles.ts | 50 ++- .../waterfall/components/translations.ts | 50 +++ .../components/use_bar_charts.test.tsx | 46 ++- .../waterfall/components/use_bar_charts.ts | 31 +- .../waterfall/components/waterfall.test.tsx | 70 ++-- .../components/waterfall_bar_chart.tsx | 112 ++++++ .../waterfall/components/waterfall_chart.tsx | 221 ++++-------- .../components/waterfall_chart_fixed_axis.tsx | 65 ++++ .../waterfall/context/waterfall_chart.tsx | 11 +- .../uptime/public/hooks/use_chart_theme.ts | 20 ++ .../public/lib/helper/enzyme_helpers.tsx | 45 ++- .../uptime/public/lib/helper/rtl_helpers.tsx | 8 +- 28 files changed, 1632 insertions(+), 472 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx create mode 100644 x-pack/plugins/uptime/public/hooks/use_chart_theme.ts diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index e9a9bb8146dbf..1db5f62823e9b 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -23,6 +23,7 @@ export { getCoreVitalsComponent, HeaderMenuPortal } from './components/shared/'; export { useTrackPageview, useUiTracker, + useTrackMetric, UiTracker, TrackMetricOptions, METRIC_TYPE, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index 487daf0332a98..a02116877f49a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -5,10 +5,143 @@ * 2.0. */ -import { colourPalette, getSeriesAndDomain } from './data_formatting'; +import { colourPalette, getSeriesAndDomain, getSidebarItems } from './data_formatting'; import { NetworkItems, MimeType } from './types'; import { WaterfallDataEntry } from '../../waterfall/types'; +const networkItems: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', + status: 200, + mimeType: 'text/css', + requestSentTime: 18098833.175, + requestStartTime: 18098835.439, + loadEndTime: 18098957.145, + timings: { + connect: 81.10800000213203, + wait: 34.577999998873565, + receive: 0.5520000013348181, + send: 0.3600000018195715, + total: 123.97000000055414, + proxy: -1, + blocked: 0.8540000017092098, + queueing: 2.263999998831423, + ssl: 55.38700000033714, + dns: 3.559999997378327, + }, + }, + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, +]; + +const networkItemsWithoutFullTimings: NetworkItems = [ + networkItems[0], + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: 2.7929999996558763, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, +]; + +const networkItemsWithoutAnyTimings: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: -1, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, +]; + +const networkItemsWithoutTimingsObject: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + }, +]; + +const networkItemsWithUncommonMimeType: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/x-javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, +]; + describe('Palettes', () => { it('A colour palette comprising timing and mime type colours is correctly generated', () => { expect(colourPalette).toEqual({ @@ -30,139 +163,6 @@ describe('Palettes', () => { }); describe('getSeriesAndDomain', () => { - const networkItems: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', - status: 200, - mimeType: 'text/css', - requestSentTime: 18098833.175, - requestStartTime: 18098835.439, - loadEndTime: 18098957.145, - timings: { - connect: 81.10800000213203, - wait: 34.577999998873565, - receive: 0.5520000013348181, - send: 0.3600000018195715, - total: 123.97000000055414, - proxy: -1, - blocked: 0.8540000017092098, - queueing: 2.263999998831423, - ssl: 55.38700000033714, - dns: 3.559999997378327, - }, - }, - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/director@1.2.8/build/director.js', - status: 200, - mimeType: 'application/javascript', - requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, - loadEndTime: 18098977.648000002, - timings: { - blocked: 84.54599999822676, - receive: 3.068000001803739, - queueing: 3.69700000010198, - proxy: -1, - total: 144.1110000014305, - wait: 52.56100000042352, - connect: -1, - send: 0.2390000008745119, - ssl: -1, - dns: -1, - }, - }, - ]; - - const networkItemsWithoutFullTimings: NetworkItems = [ - networkItems[0], - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - timings: { - total: 2.7929999996558763, - blocked: -1, - ssl: -1, - wait: -1, - connect: -1, - dns: -1, - queueing: -1, - send: -1, - proxy: -1, - receive: -1, - }, - }, - ]; - - const networkItemsWithoutAnyTimings: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - timings: { - total: -1, - blocked: -1, - ssl: -1, - wait: -1, - connect: -1, - dns: -1, - queueing: -1, - send: -1, - proxy: -1, - receive: -1, - }, - }, - ]; - - const networkItemsWithoutTimingsObject: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - }, - ]; - - const networkItemsWithUncommonMimeType: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/director@1.2.8/build/director.js', - status: 200, - mimeType: 'application/x-javascript', - requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, - loadEndTime: 18098977.648000002, - timings: { - blocked: 84.54599999822676, - receive: 3.068000001803739, - queueing: 3.69700000010198, - proxy: -1, - total: 144.1110000014305, - wait: 52.56100000042352, - connect: -1, - send: 0.2390000008745119, - ssl: -1, - dns: -1, - }, - }, - ]; - it('formats timings', () => { const actual = getSeriesAndDomain(networkItems); expect(actual).toMatchInlineSnapshot(` @@ -175,6 +175,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -188,6 +189,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#54b399", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#54b399", @@ -201,6 +203,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#da8b45", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#da8b45", @@ -214,6 +217,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#edc5a2", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#edc5a2", @@ -227,6 +231,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -240,6 +245,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -253,6 +259,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#ca8eae", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#ca8eae", @@ -266,6 +273,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -279,6 +287,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -292,6 +301,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -305,6 +315,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#9170b8", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#9170b8", @@ -316,6 +327,7 @@ describe('getSeriesAndDomain', () => { "y0": 137.70799999925657, }, ], + "totalHighlightedRequests": 2, } `); }); @@ -332,6 +344,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -345,6 +358,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#54b399", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#54b399", @@ -358,6 +372,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#da8b45", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#da8b45", @@ -371,6 +386,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#edc5a2", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#edc5a2", @@ -384,6 +400,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -397,6 +414,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -410,6 +428,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#ca8eae", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#ca8eae", @@ -423,6 +442,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#9170b8", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#9170b8", @@ -434,6 +454,7 @@ describe('getSeriesAndDomain', () => { "y0": 0.9219999983906746, }, ], + "totalHighlightedRequests": 2, } `); }); @@ -450,6 +471,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "", + "isHighlighted": true, "showTooltip": false, "tooltipProps": undefined, }, @@ -458,6 +480,7 @@ describe('getSeriesAndDomain', () => { "y0": 0, }, ], + "totalHighlightedRequests": 1, } `); }); @@ -473,6 +496,7 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { + "isHighlighted": true, "showTooltip": false, }, "x": 0, @@ -480,6 +504,7 @@ describe('getSeriesAndDomain', () => { "y0": 0, }, ], + "totalHighlightedRequests": 1, } `); }); @@ -501,4 +526,41 @@ describe('getSeriesAndDomain', () => { }); expect(contentDownloadedingConfigItem).toBeDefined(); }); + + it('counts the total number of highlighted items', () => { + // only one CSS file in this array of network Items + const actual = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); + expect(actual.totalHighlightedRequests).toBe(1); + }); + + it('adds isHighlighted to waterfall entry when filter matches', () => { + // only one CSS file in this array of network Items + const { series } = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); + series.forEach((item) => { + if (item.x === 0) { + expect(item.config.isHighlighted).toBe(true); + } else { + expect(item.config.isHighlighted).toBe(false); + } + }); + }); + + it('adds isHighlighted to waterfall entry when query matches', () => { + // only the second item matches this query + const { series } = getSeriesAndDomain(networkItems, false, 'director', []); + series.forEach((item) => { + if (item.x === 1) { + expect(item.config.isHighlighted).toBe(true); + } else { + expect(item.config.isHighlighted).toBe(false); + } + }); + }); +}); + +describe('getSidebarItems', () => { + it('passes the item index offset by 1 to offsetIndex for visual display', () => { + const actual = getSidebarItems(networkItems, false, '', []); + expect(actual[0].offsetIndex).toBe(1); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 0ac93794594c0..46f0d23d0a6b9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -55,8 +55,28 @@ const getFriendlyTooltipValue = ({ } return `${label}: ${formatValueForDisplay(value)}ms`; }; +export const isHighlightedItem = ( + item: NetworkItem, + query?: string, + activeFilters: string[] = [] +) => { + if (!query && activeFilters?.length === 0) { + return true; + } + + const matchQuery = query ? item.url?.includes(query) : true; + const matchFilters = + activeFilters.length > 0 ? activeFilters.includes(MimeTypesMap[item.mimeType!]) : true; + + return !!(matchQuery && matchFilters); +}; -export const getSeriesAndDomain = (items: NetworkItems) => { +export const getSeriesAndDomain = ( + items: NetworkItems, + onlyHighlighted = false, + query?: string, + activeFilters?: string[] +) => { const getValueForOffset = (item: NetworkItem) => { return item.requestSentTime; }; @@ -78,13 +98,21 @@ export const getSeriesAndDomain = (items: NetworkItems) => { } }; + let totalHighlightedRequests = 0; + const series = items.reduce((acc, item, index) => { + const isHighlighted = isHighlightedItem(item, query, activeFilters); + if (isHighlighted) { + totalHighlightedRequests++; + } + if (!item.timings) { acc.push({ x: index, y0: 0, y: 0, config: { + isHighlighted, showTooltip: false, }, }); @@ -96,10 +124,13 @@ export const getSeriesAndDomain = (items: NetworkItems) => { let currentOffset = offsetValue - zeroOffset; + let timingValueFound = false; + TIMING_ORDER.forEach((timing) => { const value = getValue(item.timings, timing); - const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; if (value && value >= 0) { + timingValueFound = true; + const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; const y = currentOffset + value; acc.push({ @@ -108,6 +139,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y, config: { colour, + isHighlighted, showTooltip: true, tooltipProps: { value: getFriendlyTooltipValue({ @@ -126,7 +158,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { /* if no specific timing values are found, use the total time * if total time is not available use 0, set showTooltip to false, * and omit tooltip props */ - if (!acc.find((entry) => entry.x === index)) { + if (!timingValueFound) { const total = item.timings.total; const hasTotal = total !== -1; acc.push({ @@ -134,6 +166,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y0: hasTotal ? currentOffset : 0, y: hasTotal ? currentOffset + item.timings.total : 0, config: { + isHighlighted, colour: hasTotal ? mimeTypeColour : '', showTooltip: hasTotal, tooltipProps: hasTotal @@ -154,14 +187,31 @@ export const getSeriesAndDomain = (items: NetworkItems) => { const yValues = series.map((serie) => serie.y); const domain = { min: 0, max: Math.max(...yValues) }; - return { series, domain }; + + let filteredSeries = series; + if (onlyHighlighted) { + filteredSeries = series.filter((item) => item.config.isHighlighted); + } + + return { series: filteredSeries, domain, totalHighlightedRequests }; }; -export const getSidebarItems = (items: NetworkItems): SidebarItems => { - return items.map((item) => { +export const getSidebarItems = ( + items: NetworkItems, + onlyHighlighted: boolean, + query: string, + activeFilters: string[] +): SidebarItems => { + const sideBarItems = items.map((item, index) => { + const isHighlighted = isHighlightedItem(item, query, activeFilters); + const offsetIndex = index + 1; const { url, status, method } = item; - return { url, status, method }; + return { url, status, method, isHighlighted, offsetIndex }; }); + if (onlyHighlighted) { + return sideBarItems.filter((item) => item.isHighlighted); + } + return sideBarItems; }; export const getLegendItems = (): LegendItems => { @@ -184,6 +234,7 @@ export const getLegendItems = (): LegendItems => { { name: FriendlyMimetypeLabels[mimeType], colour: MIME_TYPE_PALETTE[mimeType] }, ]; }); + return [...timingItems, ...mimeTypeItems]; }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts index 8d261edc74bf4..e22caae0d9eb2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts @@ -61,16 +61,13 @@ export const TIMING_ORDER = [ Timings.Receive, ] as const; -export type CalculatedTimings = { - [K in Timings]?: number; -}; - export enum MimeType { Html = 'html', Script = 'script', Stylesheet = 'stylesheet', Media = 'media', Font = 'font', + XHR = 'xhr', Other = 'other', } @@ -99,6 +96,9 @@ export const FriendlyMimetypeLabels = { [MimeType.Font]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.font', { defaultMessage: 'Font', }), + [MimeType.XHR]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.xhr', { + defaultMessage: 'XHR', + }), [MimeType.Other]: i18n.translate( 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.other', { @@ -112,7 +112,6 @@ export const FriendlyMimetypeLabels = { export const MimeTypesMap: Record = { 'text/html': MimeType.Html, 'application/javascript': MimeType.Script, - 'application/json': MimeType.Script, 'text/javascript': MimeType.Script, 'text/css': MimeType.Stylesheet, // Images @@ -146,38 +145,18 @@ export const MimeTypesMap: Record = { 'application/font-woff2': MimeType.Font, 'application/vnd.ms-fontobject': MimeType.Font, 'application/font-sfnt': MimeType.Font, + + // XHR + 'application/json': MimeType.XHR, }; export type NetworkItem = NetworkEvent; export type NetworkItems = NetworkItem[]; -// NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. -export interface PayloadTimings { - dns_start: number; - push_end: number; - worker_fetch_start: number; - worker_respond_with_settled: number; - proxy_end: number; - worker_start: number; - worker_ready: number; - send_end: number; - connect_end: number; - connect_start: number; - send_start: number; - proxy_start: number; - push_start: number; - ssl_end: number; - receive_headers_end: number; - ssl_start: number; - request_time: number; - dns_end: number; -} - -export interface ExtraSeriesConfig { - colour: string; -} - -export type SidebarItem = Pick; +export type SidebarItem = Pick & { + isHighlighted: boolean; + offsetIndex: number; +}; export type SidebarItems = SidebarItem[]; export interface LegendItem { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx new file mode 100644 index 0000000000000..e22f4a4c63f59 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, fireEvent } from '@testing-library/react'; +import { WaterfallChartWrapper } from './waterfall_chart_wrapper'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import { extractItems, isHighlightedItem } from './data_formatting'; + +import 'jest-canvas-mock'; +import { BAR_HEIGHT } from '../../waterfall/components/constants'; +import { MimeType } from './types'; +import { + FILTER_POPOVER_OPEN_LABEL, + FILTER_REQUESTS_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, +} from '../../waterfall/components/translations'; + +const getHighLightedItems = (query: string, filters: string[]) => { + return NETWORK_EVENTS.events.filter((item) => isHighlightedItem(item, query, filters)); +}; + +describe('waterfall chart wrapper', () => { + jest.useFakeTimers(); + + it('renders the correct sidebar items', () => { + const { getAllByTestId } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + }); + + it('search by query works', () => { + const { getAllByTestId, getByTestId, getByLabelText } = render( + + ); + + const filterInput = getByLabelText(FILTER_REQUESTS_LABEL); + + const searchText = '.js'; + + fireEvent.change(filterInput, { target: { value: searchText } }); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems(searchText, []).length; + expect(getAllByTestId('sideBarHighlightedItem')).toHaveLength(highlightedItemsLength); + + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + + const SIDE_BAR_ITEMS_HEIGHT = NETWORK_EVENTS.events.length * BAR_HEIGHT; + expect(getByTestId('wfSidebarContainer')).toHaveAttribute('height', `${SIDE_BAR_ITEMS_HEIGHT}`); + + expect(getByTestId('wfDataOnlyBarChart')).toHaveAttribute('height', `${SIDE_BAR_ITEMS_HEIGHT}`); + }); + + it('search by mime type works', () => { + const { getAllByTestId, getByLabelText, getAllByText } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getAllByText('XHR')[1]); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems('', [MimeType.XHR]).length; + + expect(getAllByTestId('sideBarHighlightedItem')).toHaveLength(highlightedItemsLength); + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + }); + + it('renders sidebar even when filter matches 0 resources', () => { + const { getAllByTestId, getByLabelText, getAllByText, queryAllByTestId } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getAllByText('CSS')[1]); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems('', [MimeType.Stylesheet]).length; + + // no CSS items found + expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + + fireEvent.click(getByLabelText(FILTER_COLLAPSE_REQUESTS_LABEL)); + + // filter bar is still accessible even when no resources match filter + expect(getByLabelText(FILTER_REQUESTS_LABEL)).toBeInTheDocument(); + + // no resources items are in the chart as none match filter + expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); + expect(queryAllByTestId('sideBarDimmedItem')).toHaveLength(0); + }); +}); + +const NETWORK_EVENTS = { + events: [ + { + timestamp: '2021-01-21T10:31:21.537Z', + method: 'GET', + url: + 'https://apv-static.minute.ly/videos/v-c2a526c7-450d-428e-1244649-a390-fb639ffead96-s45.746-54.421m.mp4', + status: 206, + mimeType: 'video/mp4', + requestSentTime: 241114127.474, + requestStartTime: 241114129.214, + loadEndTime: 241116573.402, + timings: { + total: 2445.928000001004, + queueing: 1.7399999778717756, + blocked: 0.391999987186864, + receive: 2283.964000031119, + connect: 91.5709999972023, + wait: 28.795999998692423, + proxy: -1, + dns: 36.952000024029985, + send: 0.10000000474974513, + ssl: 64.28900000173599, + }, + }, + { + timestamp: '2021-01-21T10:31:22.174Z', + method: 'GET', + url: 'https://dpm.demdex.net/ibs:dpid=73426&dpuuid=31597189268188866891125449924942215949', + status: 200, + mimeType: 'image/gif', + requestSentTime: 241114749.202, + requestStartTime: 241114750.426, + loadEndTime: 241114805.541, + timings: { + queueing: 1.2240000069141388, + receive: 2.218999987235293, + proxy: -1, + dns: -1, + send: 0.14200000441633165, + blocked: 1.033000007737428, + total: 56.33900000248104, + wait: 51.72099999617785, + ssl: -1, + connect: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.679Z', + method: 'GET', + url: 'https://dapi.cms.mlbinfra.com/v2/content/en-us/sel-t119-homepage-mediawall', + status: 200, + mimeType: 'application/json', + requestSentTime: 241114268.04299998, + requestStartTime: 241114270.184, + loadEndTime: 241114665.609, + timings: { + total: 397.5659999996424, + dns: 29.5429999823682, + wait: 221.6830000106711, + queueing: 2.1410000044852495, + connect: 106.95499999565072, + ssl: 69.06899999012239, + receive: 2.027999988058582, + blocked: 0.877000013133511, + send: 23.719999997410923, + proxy: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.runtime.b313577971db9c857801.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114303.84899998, + requestStartTime: 241114306.416, + loadEndTime: 241114370.361, + timings: { + send: 1.357000001007691, + wait: 40.12299998430535, + receive: 16.78500001435168, + ssl: -1, + queueing: 2.5670000177342445, + total: 66.51200001942925, + connect: -1, + blocked: 5.680000002030283, + proxy: -1, + dns: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.modules.7a266e7acfd42f2581a5.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114305.939, + requestStartTime: 241114310.393, + loadEndTime: 241114938.264, + timings: { + wait: 51.61500000394881, + dns: -1, + ssl: -1, + receive: 506.5750000067055, + proxy: -1, + connect: -1, + blocked: 69.51599998865277, + queueing: 4.453999979887158, + total: 632.324999984121, + send: 0.16500000492669642, + }, + }, + ], +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index 91657981e7f89..8a0e9729a635b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -5,44 +5,14 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; -import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiHealth } from '@elastic/eui'; +import { useTrackMetric, METRIC_TYPE } from '../../../../../../../observability/public'; import { getSeriesAndDomain, getSidebarItems, getLegendItems } from './data_formatting'; import { SidebarItem, LegendItem, NetworkItems } from './types'; -import { - WaterfallProvider, - WaterfallChart, - MiddleTruncatedText, - RenderItem, -} from '../../waterfall'; - -export const renderSidebarItem: RenderItem = (item, index) => { - const { status } = item; - - const isErrorStatusCode = (statusCode: number) => { - const is400 = statusCode >= 400 && statusCode <= 499; - const is500 = statusCode >= 500 && statusCode <= 599; - const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; - return is400 || is500 || isSpecific300; - }; - - return ( - <> - {!status || !isErrorStatusCode(status) ? ( - - ) : ( - - - - - - {status} - - - )} - - ); -}; +import { WaterfallProvider, WaterfallChart, RenderItem } from '../../waterfall'; +import { WaterfallFilter } from './waterfall_filter'; +import { WaterfallSidebarItem } from './waterfall_sidebar_item'; export const renderLegendItem: RenderItem = (item) => { return {item.name}; @@ -54,23 +24,64 @@ interface Props { } export const WaterfallChartWrapper: React.FC = ({ data, total }) => { + const [query, setQuery] = useState(''); + const [activeFilters, setActiveFilters] = useState([]); + const [onlyHighlighted, setOnlyHighlighted] = useState(false); + const [networkData] = useState(data); - const { series, domain } = useMemo(() => { - return getSeriesAndDomain(networkData); - }, [networkData]); + const hasFilters = activeFilters.length > 0; + + const { series, domain, totalHighlightedRequests } = useMemo(() => { + return getSeriesAndDomain(networkData, onlyHighlighted, query, activeFilters); + }, [networkData, query, activeFilters, onlyHighlighted]); const sidebarItems = useMemo(() => { - return getSidebarItems(networkData); - }, [networkData]); + return getSidebarItems(networkData, onlyHighlighted, query, activeFilters); + }, [networkData, query, activeFilters, onlyHighlighted]); const legendItems = getLegendItems(); + const renderFilter = useCallback(() => { + return ( + + ); + }, [activeFilters, setActiveFilters, onlyHighlighted, setOnlyHighlighted, query, setQuery]); + + const renderSidebarItem: RenderItem = useCallback( + (item) => { + return ( + + ); + }, + [hasFilters, onlyHighlighted] + ); + + useTrackMetric({ app: 'uptime', metric: 'waterfall_chart_view', metricType: METRIC_TYPE.COUNT }); + useTrackMetric({ + app: 'uptime', + metric: 'waterfall_chart_view', + metricType: METRIC_TYPE.COUNT, + delay: 15000, + }); + return ( { @@ -81,10 +92,19 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { tickFormat={(d: number) => `${Number(d).toFixed(0)} ms`} domain={domain} barStyleAccessor={(datum) => { + if (!datum.datum.config.isHighlighted) { + return { + rect: { + fill: datum.datum.config.colour, + opacity: '0.1', + }, + }; + } return datum.datum.config.colour; }} renderSidebarItem={renderSidebarItem} renderLegendItem={renderLegendItem} + renderFilter={renderFilter} fullHeight={true} /> diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx new file mode 100644 index 0000000000000..3acf6a269fb38 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { act, fireEvent } from '@testing-library/react'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; +import { MIME_FILTERS, WaterfallFilter } from './waterfall_filter'; +import { + FILTER_REQUESTS_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, + FILTER_POPOVER_OPEN_LABEL, +} from '../../waterfall/components/translations'; + +describe('waterfall filter', () => { + jest.useFakeTimers(); + + it('renders correctly', () => { + const { getByLabelText, getByTitle } = render( + + ); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + MIME_FILTERS.forEach((filter) => { + expect(getByTitle(filter.label)); + }); + }); + + it('filter icon changes color on active/inactive filters', () => { + const Component = () => { + const [activeFilters, setActiveFilters] = useState([]); + + return ( + + ); + }; + const { getByLabelText, getByTitle } = render(); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getByTitle('XHR')); + + expect(getByLabelText(FILTER_POPOVER_OPEN_LABEL)).toHaveAttribute( + 'class', + 'euiButtonIcon euiButtonIcon--primary' + ); + + // toggle it back to inactive + fireEvent.click(getByTitle('XHR')); + + expect(getByLabelText(FILTER_POPOVER_OPEN_LABEL)).toHaveAttribute( + 'class', + 'euiButtonIcon euiButtonIcon--text' + ); + }); + + it('search input is working properly', () => { + const setQuery = jest.fn(); + + const Component = () => { + return ( + + ); + }; + const { getByLabelText } = render(); + + const testText = 'js'; + + fireEvent.change(getByLabelText(FILTER_REQUESTS_LABEL), { target: { value: testText } }); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + expect(setQuery).toHaveBeenCalledWith(testText); + }); + + it('resets checkbox when filters are removed', () => { + const Component = () => { + const [onlyHighlighted, setOnlyHighlighted] = useState(false); + const [query, setQuery] = useState(''); + const [activeFilters, setActiveFilters] = useState([]); + return ( + + ); + }; + const { getByLabelText, getByTitle } = render(); + const input = getByLabelText(FILTER_REQUESTS_LABEL); + // apply filters + const testText = 'js'; + fireEvent.change(input, { target: { value: testText } }); + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + const filterGroupButton = getByTitle('XHR'); + fireEvent.click(filterGroupButton); + + // input has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const collapseCheckbox = getByLabelText(FILTER_COLLAPSE_REQUESTS_LABEL) as HTMLInputElement; + expect(collapseCheckbox).not.toBeDisabled(); + fireEvent.click(collapseCheckbox); + expect(collapseCheckbox).toBeChecked(); + + // remove filters + fireEvent.change(input, { target: { value: '' } }); + fireEvent.click(filterGroupButton); + + // input has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + // expect the checkbox to reset to disabled and unchecked + expect(collapseCheckbox).not.toBeChecked(); + expect(collapseCheckbox).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx new file mode 100644 index 0000000000000..42c2df4553b4c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { + EuiButtonIcon, + EuiCheckbox, + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiSpacer, +} from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; +import { + FILTER_REQUESTS_LABEL, + FILTER_SCREENREADER_LABEL, + FILTER_REMOVE_SCREENREADER_LABEL, + FILTER_POPOVER_OPEN_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, +} from '../../waterfall/components/translations'; +import { MimeType, FriendlyMimetypeLabels } from './types'; +import { METRIC_TYPE, useUiTracker } from '../../../../../../../observability/public'; + +interface Props { + query: string; + activeFilters: string[]; + setActiveFilters: Dispatch>; + setQuery: (val: string) => void; + onlyHighlighted: boolean; + setOnlyHighlighted: (val: boolean) => void; +} + +export const MIME_FILTERS = [ + { + label: FriendlyMimetypeLabels[MimeType.XHR], + mimeType: MimeType.XHR, + }, + { + label: FriendlyMimetypeLabels[MimeType.Html], + mimeType: MimeType.Html, + }, + { + label: FriendlyMimetypeLabels[MimeType.Script], + mimeType: MimeType.Script, + }, + { + label: FriendlyMimetypeLabels[MimeType.Stylesheet], + mimeType: MimeType.Stylesheet, + }, + { + label: FriendlyMimetypeLabels[MimeType.Font], + mimeType: MimeType.Font, + }, + { + label: FriendlyMimetypeLabels[MimeType.Media], + mimeType: MimeType.Media, + }, +]; + +export const WaterfallFilter = ({ + query, + setQuery, + activeFilters, + setActiveFilters, + onlyHighlighted, + setOnlyHighlighted, +}: Props) => { + const [value, setValue] = useState(query); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const trackMetric = useUiTracker({ app: 'uptime' }); + + const toggleFilters = (val: string) => { + setActiveFilters((prevState) => + prevState.includes(val) ? prevState.filter((filter) => filter !== val) : [...prevState, val] + ); + }; + useDebounce( + () => { + setQuery(value); + }, + 250, + [value] + ); + + /* reset checkbox when there is no query or active filters + * this prevents the checkbox from being checked in a disabled state */ + useEffect(() => { + if (!(query || activeFilters.length > 0)) { + setOnlyHighlighted(false); + } + }, [activeFilters.length, setOnlyHighlighted, query]); + + // indicates use of the query input box + useEffect(() => { + if (query) { + trackMetric({ metric: 'waterfall_filter_input_changed', metricType: METRIC_TYPE.CLICK }); + } + }, [query, trackMetric]); + + // indicates the collapse to show only highlighted checkbox has been clicked + useEffect(() => { + if (onlyHighlighted) { + trackMetric({ + metric: 'waterfall_filter_collapse_checked', + metricType: METRIC_TYPE.CLICK, + }); + } + }, [onlyHighlighted, trackMetric]); + + // indicates filters have been applied or changed + useEffect(() => { + if (activeFilters.length > 0) { + trackMetric({ + metric: `waterfall_filters_applied_changed`, + metricType: METRIC_TYPE.CLICK, + }); + } + }, [activeFilters, trackMetric]); + + return ( + + + { + setValue(evt.target.value); + }} + value={value} + /> + + + setIsPopoverOpen((prevState) => !prevState)} + color={activeFilters.length > 0 ? 'primary' : 'text'} + isSelected={activeFilters.length > 0} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="rightCenter" + > + + {MIME_FILTERS.map(({ label, mimeType }) => ( + toggleFilters(mimeType)} + key={label} + withNext={true} + aria-label={`${ + activeFilters.includes(mimeType) + ? FILTER_REMOVE_SCREENREADER_LABEL + : FILTER_SCREENREADER_LABEL + } ${label}`} + > + {label} + + ))} + + + 0)} + id="onlyHighlighted" + label={FILTER_COLLAPSE_REQUESTS_LABEL} + checked={onlyHighlighted} + onChange={(e) => { + setOnlyHighlighted(e.target.checked); + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx new file mode 100644 index 0000000000000..25b577ef9403a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { SidebarItem } from '../waterfall/types'; +import { MiddleTruncatedText } from '../../waterfall'; +import { SideBarItemHighlighter } from '../../waterfall/components/styles'; +import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; + +interface SidebarItemProps { + item: SidebarItem; + renderFilterScreenReaderText?: boolean; +} + +export const WaterfallSidebarItem = ({ item, renderFilterScreenReaderText }: SidebarItemProps) => { + const { status, offsetIndex, isHighlighted } = item; + + const isErrorStatusCode = (statusCode: number) => { + const is400 = statusCode >= 400 && statusCode <= 499; + const is500 = statusCode >= 500 && statusCode <= 599; + const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; + return is400 || is500 || isSpecific300; + }; + + const text = `${offsetIndex}. ${item.url}`; + const ariaLabel = `${ + isHighlighted && renderFilterScreenReaderText + ? `${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ` + : '' + }${text}`; + + return ( + + {!status || !isErrorStatusCode(status) ? ( + + ) : ( + + + + + + {status} + + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx new file mode 100644 index 0000000000000..578d66a1ea3f1 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SidebarItem } from '../waterfall/types'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; +import { WaterfallSidebarItem } from './waterfall_sidebar_item'; +import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; + +describe('waterfall filter', () => { + const url = 'http://www.elastic.co'; + const offsetIndex = 1; + const item: SidebarItem = { + url, + isHighlighted: true, + offsetIndex, + }; + + it('renders sidbar item', () => { + const { getByText } = render(); + + expect(getByText(`${offsetIndex}. ${url}`)); + }); + + it('render screen reader text when renderFilterScreenReaderText is true', () => { + const { getByLabelText } = render( + + ); + + expect( + getByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + ).toBeInTheDocument(); + }); + + it('does not render screen reader text when renderFilterScreenReaderText is false', () => { + const { queryByLabelText } = render( + + ); + + expect( + queryByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + ).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts index 543d6004b8955..a4b75174543a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts @@ -17,3 +17,5 @@ export const FIXED_AXIS_HEIGHT = 32; // number of items to display in canvas, since canvas can only have limited size export const CANVAS_MAX_ITEMS = 150; + +export const CHART_LEGEND_PADDING = 62; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx index 9a3d4efb63a3a..d6c1d777a40a7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx @@ -25,15 +25,21 @@ describe('getChunks', () => { }); describe('Component', () => { - it('renders truncated text', () => { - const { getByText } = render(); + it('renders truncated text and aria label', () => { + const { getByText, getByLabelText } = render( + + ); expect(getByText(first)).toBeInTheDocument(); expect(getByText(last)).toBeInTheDocument(); + + expect(getByLabelText(longString)).toBeInTheDocument(); }); it('renders screen reader only text', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const { getByText } = within(getByTestId('middleTruncatedTextSROnly')); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx index 9c263312f78f5..ec363ed2b40a4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -10,6 +10,11 @@ import styled from 'styled-components'; import { EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui'; import { FIXED_AXIS_HEIGHT } from './constants'; +interface Props { + ariaLabel: string; + text: string; +} + const OuterContainer = styled.div` width: 100%; height: 100%; @@ -50,14 +55,14 @@ export const getChunks = (text: string) => { // Helper component for adding middle text truncation, e.g. // really-really-really-long....ompressed.js // Can be used to accomodate content in sidebar item rendering. -export const MiddleTruncatedText = ({ text }: { text: string }) => { +export const MiddleTruncatedText = ({ ariaLabel, text }: Props) => { const chunks = useMemo(() => { return getChunks(text); }, [text]); return ( <> - + {text} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx index f46bab8c33a85..63b4d2945a51c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx @@ -12,7 +12,11 @@ import { render } from '../../../../../lib/helper/rtl_helpers'; describe('NetworkRequestsTotal', () => { it('message in case total is greater than fetched', () => { const { getByText, getByLabelText } = render( - + ); expect(getByText('First 1000/1100 network requests')).toBeInTheDocument(); @@ -21,9 +25,52 @@ describe('NetworkRequestsTotal', () => { it('message in case total is equal to fetched requests', () => { const { getByText } = render( - + ); expect(getByText('500 network requests')).toBeInTheDocument(); }); + + it('does not show highlighted item message when showHighlightedNetworkEvents is false', () => { + const { queryByText } = render( + + ); + + expect(queryByText(/match the filter/)).not.toBeInTheDocument(); + }); + + it('does not show highlighted item message when highlightedNetworkEvents is less than 0', () => { + const { queryByText } = render( + + ); + + expect(queryByText(/match the filter/)).not.toBeInTheDocument(); + }); + + it('show highlighted item message when highlightedNetworkEvents is greater than 0 and showHighlightedNetworkEvents is true', () => { + const { getByText } = render( + + ); + + expect(getByText(/\(20 match the filter\)/)).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx index fce86c6b5c29d..5ccd60b0ce7a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiIconTip } from '@elastic/eui'; import { NetworkRequestsTotalStyle } from './styles'; @@ -13,24 +14,44 @@ import { NetworkRequestsTotalStyle } from './styles'; interface Props { totalNetworkRequests: number; fetchedNetworkRequests: number; + highlightedNetworkRequests: number; + showHighlightedNetworkRequests?: boolean; } -export const NetworkRequestsTotal = ({ totalNetworkRequests, fetchedNetworkRequests }: Props) => { +export const NetworkRequestsTotal = ({ + totalNetworkRequests, + fetchedNetworkRequests, + highlightedNetworkRequests, + showHighlightedNetworkRequests, +}: Props) => { return ( - {i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage', { - defaultMessage: '{numNetworkRequests} network requests', - values: { + fetchedNetworkRequests - ? i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage.first', { - defaultMessage: 'First {count}', - values: { count: `${fetchedNetworkRequests}/${totalNetworkRequests}` }, - }) - : totalNetworkRequests, - }, - })} + totalNetworkRequests > fetchedNetworkRequests ? ( + + ) : ( + totalNetworkRequests + ), + }} + />{' '} + {showHighlightedNetworkRequests && highlightedNetworkRequests >= 0 && ( + + )} {totalNetworkRequests > fetchedNetworkRequests && ( = ({ items, render }) => { return ( - + - {items.map((item, index) => { - return ( - - {render(item, index)} - - ); - })} + {items.map((item) => ( + + {render(item)} + + ))} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 333acd6e043df..c00c04b114045 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -14,10 +14,7 @@ interface WaterfallChartOuterContainerProps { height?: string; } -export const WaterfallChartOuterContainer = euiStyled.div` - height: ${(props) => (props.height ? `${props.height}` : 'auto')}; - overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; - overflow-x: hidden; +const StyledScrollDiv = euiStyled.div` &::-webkit-scrollbar { height: ${({ theme }) => theme.eui.euiScrollBar}; width: ${({ theme }) => theme.eui.euiScrollBar}; @@ -33,11 +30,27 @@ export const WaterfallChartOuterContainer = euiStyled.div` + height: ${(props) => (props.height ? `${props.height}` : 'auto')}; + overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; + overflow-x: hidden; +`; + +export const WaterfallChartFixedTopContainer = euiStyled(StyledScrollDiv)` position: sticky; top: 0; z-index: ${(props) => props.theme.eui.euiZLevel4}; - border-bottom: ${(props) => `1px solid ${props.theme.eui.euiColorLightShade}`}; + overflow-y: scroll; + overflow-x: hidden; +`; + +export const WaterfallChartAxisOnlyContainer = euiStyled(EuiFlexItem)` + margin-left: -22px; +`; + +export const WaterfallChartTopContainer = euiStyled(EuiFlexGroup)` `; export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` @@ -46,9 +59,18 @@ export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` border: none; `; // NOTE: border-radius !important is here as the "border" prop isn't working +export const WaterfallChartFilterContainer = euiStyled.div` + && { + padding: 16px; + z-index: ${(props) => props.theme.eui.euiZLevel5}; + border-bottom: 0.3px solid ${(props) => props.theme.eui.euiColorLightShade}; + } +`; // NOTE: border-radius !important is here as the "border" prop isn't working + export const WaterfallChartFixedAxisContainer = euiStyled.div` height: ${FIXED_AXIS_HEIGHT}px; z-index: ${(props) => props.theme.eui.euiZLevel4}; + height: 100%; `; interface WaterfallChartSidebarContainer { @@ -74,6 +96,12 @@ export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)` min-width: 0; padding-left: ${(props) => props.theme.eui.paddingSizes.m}; padding-right: ${(props) => props.theme.eui.paddingSizes.m}; + z-index: ${(props) => props.theme.eui.euiZLevel4}; +`; + +export const SideBarItemHighlighter = euiStyled.span<{ isHighlighted: boolean }>` + opacity: ${(props) => (props.isHighlighted ? 1 : 0.4)}; + height: 100%; `; interface WaterfallChartChartContainer { @@ -106,6 +134,12 @@ export const WaterfallChartTooltip = euiStyled.div` `; export const NetworkRequestsTotalStyle = euiStyled(EuiText)` - line-height: ${FIXED_AXIS_HEIGHT}px; - margin-left: ${(props) => props.theme.eui.paddingSizes.m} + line-height: 28px; + padding: 0 ${(props) => props.theme.eui.paddingSizes.m}; + border-bottom: 0.3px solid ${(props) => props.theme.eui.euiColorLightShade}; + z-index: ${(props) => props.theme.eui.euiZLevel5}; +`; + +export const RelativeContainer = euiStyled.div` + position: relative; `; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts new file mode 100644 index 0000000000000..b63ffacaadd2e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FILTER_REQUESTS_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.searchBox.placeholder', + { + defaultMessage: 'Filter network requests', + } +); + +export const FILTER_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.filterGroup.filterScreenreaderLabel', + { + defaultMessage: 'Filter by', + } +); + +export const FILTER_REMOVE_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.filterGroup.removeFilterScreenReaderLabel', + { + defaultMessage: 'Remove filter by', + } +); + +export const FILTER_POPOVER_OPEN_LABEL = i18n.translate( + 'xpack.uptime.pingList.synthetics.waterfall.filters.popover', + { + defaultMessage: 'Click to open waterfall filters', + } +); + +export const FILTER_COLLAPSE_REQUESTS_LABEL = i18n.translate( + 'xpack.uptime.pingList.synthetics.waterfall.filters.collapseRequestsLabel', + { + defaultMessage: 'Collapse to only show matching requests', + } +); + +export const SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.sidebar.filterMatchesScreenReaderLabel', + { + defaultMessage: 'Resource matches filter', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx index 1ce46fc0d6e7b..a963fb1e2939c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx @@ -10,9 +10,14 @@ import { renderHook } from '@testing-library/react-hooks'; import { IWaterfallContext } from '../context/waterfall_chart'; import { CANVAS_MAX_ITEMS } from './constants'; -const generateTestData = (): IWaterfallContext['data'] => { +const generateTestData = ( + { + xMultiplier, + }: { + xMultiplier: number; + } = { xMultiplier: 1 } +): IWaterfallContext['data'] => { const numberOfItems = 1000; - const data: IWaterfallContext['data'] = []; const testItem = { x: 0, @@ -29,11 +34,11 @@ const generateTestData = (): IWaterfallContext['data'] => { data.push( { ...testItem, - x: i, + x: xMultiplier * i, }, { ...testItem, - x: i, + x: xMultiplier * i, y0: 7, y: 25, } @@ -44,7 +49,7 @@ const generateTestData = (): IWaterfallContext['data'] => { }; describe('useBarChartsHooks', () => { - it('returns result as expected', () => { + it('returns result as expected for non filtered data', () => { const { result, rerender } = renderHook((props) => useBarCharts(props), { initialProps: { data: [] as IWaterfallContext['data'] }, }); @@ -70,4 +75,35 @@ describe('useBarChartsHooks', () => { expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4); expect(lastChartItems[lastChartItems.length - 1].x).toBe(CANVAS_MAX_ITEMS * 5 - 1); }); + + it('returns result as expected for filtered data', () => { + /* multiply x values to simulate filtered data, where x values can have gaps in the + * sequential order */ + const xMultiplier = 2; + const { result, rerender } = renderHook((props) => useBarCharts(props), { + initialProps: { data: [] as IWaterfallContext['data'] }, + }); + + expect(result.current).toHaveLength(0); + const newData = generateTestData({ xMultiplier }); + + rerender({ data: newData }); + + // Thousands items will result in 7 Canvas + expect(result.current.length).toBe(7); + + const firstChartItems = result.current[0]; + const lastChartItems = result.current[4]; + + // first chart items last item should be x 149, since we only display 150 items + expect(firstChartItems[firstChartItems.length - 1].x).toBe( + (CANVAS_MAX_ITEMS - 1) * xMultiplier + ); + + // since here are 5 charts, last chart first item should be x 600 + expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4 * xMultiplier); + expect(lastChartItems[lastChartItems.length - 1].x).toBe( + (CANVAS_MAX_ITEMS * 5 - 1) * xMultiplier + ); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts index 79fd437039afe..2baf895504911 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts @@ -13,27 +13,36 @@ export interface UseBarHookProps { data: IWaterfallContext['data']; } -export const useBarCharts = ({ data = [] }: UseBarHookProps) => { +export const useBarCharts = ({ data }: UseBarHookProps) => { const [charts, setCharts] = useState>([]); useEffect(() => { - if (data.length > 0) { - let chartIndex = 0; - - const chartsN: Array = []; + const chartsN: Array = []; + if (data?.length > 0) { + let chartIndex = 0; + /* We want at most CANVAS_MAX_ITEMS **RESOURCES** per array. + * Resources !== individual timing items, but are comprised of many individual timing + * items. The X value of each item can be used as an id for the resource. + * We must keep track of the number of unique resources added to the each array. */ + const uniqueResources = new Set(); + let lastIndex: number; data.forEach((item) => { - // Subtract 1 to account for x value starting from 0 - if (item.x === CANVAS_MAX_ITEMS * chartIndex && !chartsN[item.x / CANVAS_MAX_ITEMS]) { - chartsN.push([item]); + if (uniqueResources.size === CANVAS_MAX_ITEMS && item.x > lastIndex) { chartIndex++; + uniqueResources.clear(); + } + uniqueResources.add(item.x); + lastIndex = item.x; + if (!chartsN[chartIndex]) { + chartsN.push([item]); return; } - chartsN[chartIndex - 1].push(item); + chartsN[chartIndex].push(item); }); - - setCharts(chartsN); } + + setCharts(chartsN); }, [data]); return charts; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx index 7c9051e8f6acf..528d749f576fc 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx @@ -6,64 +6,38 @@ */ import React from 'react'; -import { of } from 'rxjs'; -import { MountWithReduxProvider, mountWithRouter } from '../../../../../lib'; -import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; import { WaterfallChart } from './waterfall_chart'; -import { - renderLegendItem, - renderSidebarItem, -} from '../../step_detail/waterfall/waterfall_chart_wrapper'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { WaterfallChartOuterContainer } from './styles'; +import { renderLegendItem } from '../../step_detail/waterfall/waterfall_chart_wrapper'; +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; describe('waterfall', () => { it('sets the correct height in case of full height', () => { - const core = mockCore(); - const Component = () => { return ( - `${Number(d).toFixed(0)} ms`} - domain={{ - max: 3371, - min: 0, - }} - barStyleAccessor={(datum) => { - return datum.datum.config.colour; - }} - renderSidebarItem={renderSidebarItem} - renderLegendItem={renderLegendItem} - fullHeight={true} - /> +
+ `${Number(d).toFixed(0)} ms`} + domain={{ + max: 3371, + min: 0, + }} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={undefined} + renderLegendItem={renderLegendItem} + fullHeight={true} + /> +
); }; - const component = mountWithRouter( - - - - - - - - ); + const { getByTestId } = render(); - const chartWrapper = component.find(WaterfallChartOuterContainer); + const chartWrapper = getByTestId('waterfallOuterContainer'); - expect(chartWrapper.get(0).props.height).toBe('calc(100vh - 0px)'); + expect(chartWrapper).toHaveStyleRule('height', 'calc(100vh - 62px)'); }); }); - -const mockCore: () => any = () => { - return { - application: { - getUrlForApp: () => '/app/uptime', - navigateToUrl: jest.fn(), - }, - uiSettings: { - get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', - get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), - }, - }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx new file mode 100644 index 0000000000000..df00df147fc6c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + Axis, + BarSeries, + BarStyleAccessor, + Chart, + DomainRange, + Position, + ScaleType, + Settings, + TickFormatter, + TooltipInfo, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { BAR_HEIGHT } from './constants'; +import { useChartTheme } from '../../../../../hooks/use_chart_theme'; +import { WaterfallChartChartContainer, WaterfallChartTooltip } from './styles'; +import { useWaterfallContext, WaterfallData } from '..'; + +const getChartHeight = (data: WaterfallData): number => { + // We get the last item x(number of bars) and adds 1 to cater for 0 index + const noOfXBars = new Set(data.map((item) => item.x)).size; + + return noOfXBars * BAR_HEIGHT; +}; + +const Tooltip = (tooltipInfo: TooltipInfo) => { + const { data, renderTooltipItem } = useWaterfallContext(); + const relevantItems = data.filter((item) => { + return ( + item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps + ); + }); + return relevantItems.length ? ( + + + {relevantItems.map((item, index) => { + return ( + {renderTooltipItem(item.config.tooltipProps)} + ); + })} + + + ) : null; +}; + +interface Props { + index: number; + chartData: WaterfallData; + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; +} + +export const WaterfallBarChart = ({ + chartData, + tickFormat, + domain, + barStyleAccessor, + index, +}: Props) => { + const theme = useChartTheme(); + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index 8f831d0629b25..e0e5165b41e49 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -5,62 +5,30 @@ * 2.0. */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { - Axis, - BarSeries, - Chart, - Position, - ScaleType, - Settings, - TickFormatter, - DomainRange, - BarStyleAccessor, - TooltipInfo, - TooltipType, -} from '@elastic/charts'; -import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -// NOTE: The WaterfallChart has a hard requirement that consumers / solutions are making use of KibanaReactContext, and useKibana etc -// can therefore be accessed. -import { useUiSetting$ } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { TickFormatter, DomainRange, BarStyleAccessor } from '@elastic/charts'; + import { useWaterfallContext } from '../context/waterfall_chart'; import { WaterfallChartOuterContainer, WaterfallChartFixedTopContainer, WaterfallChartFixedTopContainerSidebarCover, - WaterfallChartFixedAxisContainer, - WaterfallChartChartContainer, - WaterfallChartTooltip, + WaterfallChartTopContainer, + RelativeContainer, + WaterfallChartFilterContainer, + WaterfallChartAxisOnlyContainer, } from './styles'; -import { WaterfallData } from '../types'; -import { BAR_HEIGHT, CANVAS_MAX_ITEMS, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; +import { CHART_LEGEND_PADDING, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; import { Sidebar } from './sidebar'; import { Legend } from './legend'; import { useBarCharts } from './use_bar_charts'; +import { WaterfallBarChart } from './waterfall_bar_chart'; +import { WaterfallChartFixedAxis } from './waterfall_chart_fixed_axis'; import { NetworkRequestsTotal } from './network_requests_total'; -const Tooltip = (tooltipInfo: TooltipInfo) => { - const { data, renderTooltipItem } = useWaterfallContext(); - const relevantItems = data.filter((item) => { - return ( - item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps - ); - }); - return relevantItems.length ? ( - - - {relevantItems.map((item, index) => { - return ( - {renderTooltipItem(item.config.tooltipProps)} - ); - })} - - - ) : null; -}; - -export type RenderItem = (item: I, index: number) => JSX.Element; +export type RenderItem = (item: I, index?: number) => JSX.Element; +export type RenderFilter = () => JSX.Element; export interface WaterfallChartProps { tickFormat: TickFormatter; @@ -68,159 +36,100 @@ export interface WaterfallChartProps { barStyleAccessor: BarStyleAccessor; renderSidebarItem?: RenderItem; renderLegendItem?: RenderItem; + renderFilter?: RenderFilter; maxHeight?: string; fullHeight?: boolean; } -const getChartHeight = (data: WaterfallData, ind: number): number => { - // We get the last item x(number of bars) and adds 1 to cater for 0 index - return (data[data.length - 1]?.x + 1 - ind * CANVAS_MAX_ITEMS) * BAR_HEIGHT; -}; - export const WaterfallChart = ({ tickFormat, domain, barStyleAccessor, renderSidebarItem, renderLegendItem, + renderFilter, maxHeight = '800px', fullHeight = false, }: WaterfallChartProps) => { const { data, + showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, totalNetworkRequests, + highlightedNetworkRequests, fetchedNetworkRequests, } = useWaterfallContext(); - const [darkMode] = useUiSetting$('theme:darkMode'); - - const theme = useMemo(() => { - return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - }, [darkMode]); - const chartWrapperDivRef = useRef(null); const [height, setHeight] = useState(maxHeight); - const shouldRenderSidebar = !!(sidebarItems && sidebarItems.length > 0 && renderSidebarItem); + const shouldRenderSidebar = !!(sidebarItems && renderSidebarItem); const shouldRenderLegend = !!(legendItems && legendItems.length > 0 && renderLegendItem); useEffect(() => { if (fullHeight && chartWrapperDivRef.current) { const chartOffset = chartWrapperDivRef.current.getBoundingClientRect().top; - setHeight(`calc(100vh - ${chartOffset}px)`); + setHeight(`calc(100vh - ${chartOffset + CHART_LEGEND_PADDING}px)`); } }, [chartWrapperDivRef, fullHeight]); const chartsToDisplay = useBarCharts({ data }); return ( - - <> - - - {shouldRenderSidebar && ( - - - - - - )} - - - - - - - - - - + + + + {shouldRenderSidebar && ( + + + + {renderFilter && ( + {renderFilter()} + )} - - - + )} + + + + + + + + {shouldRenderSidebar && } - + + {chartsToDisplay.map((chartData, ind) => ( - - - - - - - - - + chartData={chartData} + domain={domain} + barStyleAccessor={barStyleAccessor} + tickFormat={tickFormat} + /> ))} - + - {shouldRenderLegend && } - - + + {shouldRenderLegend && } + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx new file mode 100644 index 0000000000000..3a7ab421b6277 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + Axis, + BarSeries, + BarStyleAccessor, + Chart, + DomainRange, + Position, + ScaleType, + Settings, + TickFormatter, + TooltipType, +} from '@elastic/charts'; +import { useChartTheme } from '../../../../../hooks/use_chart_theme'; +import { WaterfallChartFixedAxisContainer } from './styles'; + +interface Props { + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; +} + +export const WaterfallChartFixedAxis = ({ tickFormat, domain, barStyleAccessor }: Props) => { + const theme = useChartTheme(); + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx index 68d24514a37d3..9e87d69ce38a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -7,12 +7,15 @@ import React, { createContext, useContext, Context } from 'react'; import { WaterfallData, WaterfallDataEntry } from '../types'; +import { SidebarItems } from '../../step_detail/waterfall/types'; export interface IWaterfallContext { totalNetworkRequests: number; + highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: WaterfallData; - sidebarItems?: unknown[]; + showOnlyHighlightedNetworkRequests: boolean; + sidebarItems?: SidebarItems; legendItems?: unknown[]; renderTooltipItem: ( item: WaterfallDataEntry['config']['tooltipProps'], @@ -24,8 +27,10 @@ export const WaterfallContext = createContext>({}); interface ProviderProps { totalNetworkRequests: number; + highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: IWaterfallContext['data']; + showOnlyHighlightedNetworkRequests: IWaterfallContext['showOnlyHighlightedNetworkRequests']; sidebarItems?: IWaterfallContext['sidebarItems']; legendItems?: IWaterfallContext['legendItems']; renderTooltipItem: IWaterfallContext['renderTooltipItem']; @@ -34,20 +39,24 @@ interface ProviderProps { export const WaterfallProvider: React.FC = ({ children, data, + showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, renderTooltipItem, totalNetworkRequests, + highlightedNetworkRequests, fetchedNetworkRequests, }) => { return ( diff --git a/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts b/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts new file mode 100644 index 0000000000000..f9231abaa75a8 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +import { useMemo } from 'react'; +import { useUiSetting$ } from '../../../../../src/plugins/kibana_react/public'; + +export const useChartTheme = () => { + const [darkMode] = useUiSetting$('theme:darkMode'); + + const theme = useMemo(() => { + return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; + }, [darkMode]); + + return theme; +}; diff --git a/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx index 9656c63274a13..4c81247fb2cf1 100644 --- a/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx @@ -8,10 +8,17 @@ import React, { ReactElement } from 'react'; import { Router } from 'react-router-dom'; import { MemoryHistory } from 'history/createMemoryHistory'; -import { createMemoryHistory } from 'history'; +import { createMemoryHistory, History } from 'history'; import { mountWithIntl, renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { MountWithReduxProvider } from './helper_with_redux'; import { AppState } from '../../state'; +import { mockState } from '../__mocks__/uptime_store.mock'; +import { KibanaProviderOptions, MockRouter } from './rtl_helpers'; + +interface RenderRouterOptions extends KibanaProviderOptions { + history?: History; + state?: Partial; +} const helperWithRouter: ( helper: (node: ReactElement) => R, @@ -67,3 +74,39 @@ export const mountWithRouterRedux = ( options?.storeState ); }; + +/* Custom enzyme render */ +export function render( + ui: ReactElement, + { history, core, kibanaProps, state }: RenderRouterOptions = {} +) { + const testState: AppState = { + ...mockState, + ...state, + }; + return renderWithIntl( + + + {ui} + + + ); +} + +/* Custom enzyme render */ +export function mount( + ui: ReactElement, + { history, core, kibanaProps, state }: RenderRouterOptions = {} +) { + const testState: AppState = { + ...mockState, + ...state, + }; + return mountWithIntl( + + + {ui} + + + ); +} diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index abc0451bf8efa..e02a2c6f9832f 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -6,6 +6,7 @@ */ import React, { ReactElement } from 'react'; +import { of } from 'rxjs'; import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; import { Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; @@ -26,7 +27,7 @@ interface KibanaProps { services?: KibanaServices; } -interface KibanaProviderOptions { +export interface KibanaProviderOptions { core?: Partial & ExtraCore; kibanaProps?: KibanaProps; } @@ -54,6 +55,11 @@ const mockCore: () => any = () => { getUrlForApp: () => '/app/uptime', navigateToUrl: jest.fn(), }, + uiSettings: { + get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', + get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), + }, + usageCollection: { reportUiCounter: () => {} }, }; return core; From 31a3ec5934b0eb566d66797d43518933599fbfdc Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 8 Feb 2021 15:31:09 -0500 Subject: [PATCH 023/100] [Time To Visualize] Make State Transfer App Specific (#89804) * made state transfer app specific --- ...mbeddablestatetransfer.cleareditorstate.md | 11 ++- ...blestatetransfer.getincomingeditorstate.md | 5 +- ...tetransfer.getincomingembeddablepackage.md | 5 +- ...beddable-public.embeddablestatetransfer.md | 6 +- .../hooks/use_dashboard_container.ts | 6 +- .../embeddable_state_transfer.test.ts | 98 ++++++++++++++++--- .../embeddable_state_transfer.ts | 39 ++++++-- src/plugins/embeddable/public/public.api.md | 7 +- .../components/visualize_byvalue_editor.tsx | 2 +- .../components/visualize_editor.tsx | 4 +- .../components/visualize_listing.tsx | 2 +- .../application/utils/get_top_nav_config.tsx | 2 +- .../public/application/visualize_constants.ts | 1 + src/plugins/visualize/public/plugin.ts | 6 +- x-pack/plugins/lens/common/constants.ts | 1 + x-pack/plugins/lens/public/app_plugin/app.tsx | 4 +- .../lens/public/app_plugin/mounter.tsx | 4 +- x-pack/plugins/lens/public/plugin.ts | 4 +- x-pack/plugins/maps/public/render_app.tsx | 3 +- .../routes/list_page/load_list_and_render.tsx | 4 +- .../routes/map_page/saved_map/saved_map.ts | 4 +- 21 files changed, 162 insertions(+), 56 deletions(-) diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md index 5c1a6a0393c2e..034f9c70e389f 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md @@ -4,11 +4,20 @@ ## EmbeddableStateTransfer.clearEditorState() method +Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id + Signature: ```typescript -clearEditorState(): void; +clearEditorState(appId: string): void; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| appId | string | The app to fetch incomingEditorState for | + Returns: `void` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md index 1434de2c9870e..cd261bff5905b 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md @@ -4,18 +4,19 @@ ## EmbeddableStateTransfer.getIncomingEditorState() method -Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage +Fetches an [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id Signature: ```typescript -getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined; +getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| appId | string | The app to fetch incomingEditorState for | | removeAfterFetch | boolean | Whether to remove the package state after fetch to prevent duplicates. | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md index 9ead71f0bb22c..47873c8e91e41 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md @@ -4,18 +4,19 @@ ## EmbeddableStateTransfer.getIncomingEmbeddablePackage() method -Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage +Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) from the sessionStorage for the given AppId Signature: ```typescript -getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined; +getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| appId | string | The app to fetch EmbeddablePackageState for | | removeAfterFetch | boolean | Whether to remove the package state after fetch to prevent duplicates. | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md index 76b6708b93bd1..13c6c8c0325f1 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md @@ -29,9 +29,9 @@ export declare class EmbeddableStateTransfer | Method | Modifiers | Description | | --- | --- | --- | -| [clearEditorState()](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | | -| [getIncomingEditorState(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage | -| [getIncomingEmbeddablePackage(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage | +| [clearEditorState(appId)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id | +| [getIncomingEditorState(appId, removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id | +| [getIncomingEmbeddablePackage(appId, removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) from the sessionStorage for the given AppId | | [navigateToEditor(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetoeditor.md) | | A wrapper around the method which navigates to the specified appId with [embeddable editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) | | [navigateToWithEmbeddablePackage(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetowithembeddablepackage.md) | | A wrapper around the method which navigates to the specified appId with [embeddable package state](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) | diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts index b27322b6bec53..d12fea07bdd41 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts @@ -21,7 +21,7 @@ import { import { DashboardStateManager } from '../dashboard_state_manager'; import { getDashboardContainerInput, getSearchSessionIdFromURL } from '../dashboard_app_functions'; -import { DashboardContainer, DashboardContainerInput } from '../..'; +import { DashboardConstants, DashboardContainer, DashboardContainerInput } from '../..'; import { DashboardAppServices } from '../types'; import { DASHBOARD_CONTAINER_TYPE } from '..'; @@ -68,7 +68,9 @@ export const useDashboardContainer = ( searchSession.restore(searchSessionIdFromURL); } - const incomingEmbeddable = embeddable.getStateTransfer().getIncomingEmbeddablePackage(true); + const incomingEmbeddable = embeddable + .getStateTransfer() + .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, true); let canceled = false; let pendingContainer: DashboardContainer | ErrorEmbeddable | null | undefined; diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts index 763186fc17c0c..a8ecb384f782b 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts @@ -42,6 +42,10 @@ describe('embeddable state transfer', () => { const destinationApp = 'superUltraVisualize'; const originatingApp = 'superUltraTestDashboard'; + const testAppId = 'testApp'; + + const buildKey = (appId: string, key: string) => `${appId}-${key}`; + beforeEach(() => { currentAppId$ = new Subject(); currentAppId$.next(originatingApp); @@ -82,7 +86,9 @@ describe('embeddable state transfer', () => { it('can send an outgoing editor state', async () => { await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -98,7 +104,9 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -117,7 +125,10 @@ describe('embeddable state transfer', () => { state: { type: 'coolestType', input: { savedObjectId: '150' } }, }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -133,7 +144,10 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -151,42 +165,92 @@ describe('embeddable state transfer', () => { it('can fetch an incoming editor state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, + }); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); + expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' }); + }); + + it('can fetch an incoming editor state and ignore state for other apps', async () => { + store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { + [buildKey('otherApp1', EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'whoops not me', + }, + [buildKey('otherApp2', EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'otherTestDashboard', + }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); - const fetchedState = stateTransfer.getIncomingEditorState(); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' }); + + const fetchedState2 = stateTransfer.getIncomingEditorState('otherApp2'); + expect(fetchedState2).toEqual({ originatingApp: 'otherTestDashboard' }); }); it('incoming editor state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { helloSportsKibana: 'superUltraTestDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + helloSportsKibana: 'superUltraTestDashboard', + }, }); - const fetchedState = stateTransfer.getIncomingEditorState(); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); expect(fetchedState).toBeUndefined(); }); it('can fetch an incoming embeddable package state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'skisEmbeddable', input: { savedObjectId: '123' } }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, }); - const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); }); + it('can fetch an incoming embeddable package state and ignore state for other apps', async () => { + store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, + [buildKey('testApp2', EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }, + }); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); + expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); + + const fetchedState2 = stateTransfer.getIncomingEmbeddablePackage('testApp2'); + expect(fetchedState2).toEqual({ + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }); + }); + it('embeddable package state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { kibanaIsFor: 'sports' }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { kibanaIsFor: 'sports' }, }); - const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toBeUndefined(); }); it('removes embeddable package key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, iSHouldStillbeHere: 'doing the sports thing', }); - stateTransfer.getIncomingEmbeddablePackage(true); + stateTransfer.getIncomingEmbeddablePackage(testAppId, true); expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({ iSHouldStillbeHere: 'doing the sports thing', }); @@ -194,10 +258,12 @@ describe('embeddable state transfer', () => { it('removes editor state key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superCoolFootballDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superCoolFootballDashboard', + }, iSHouldStillbeHere: 'doing the sports thing', }); - stateTransfer.getIncomingEditorState(true); + stateTransfer.getIncomingEditorState(testAppId, true); expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({ iSHouldStillbeHere: 'doing the sports thing', }); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts index d3b1c1c76aadf..8664a5aae7345 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts @@ -50,13 +50,18 @@ export class EmbeddableStateTransfer { public getAppNameFromId = (appId: string): string | undefined => this.appList?.get(appId)?.title; /** - * Fetches an {@link EmbeddableEditorState | originating app} argument from the sessionStorage + * Fetches an {@link EmbeddableEditorState | editor state} from the sessionStorage for the provided app id * + * @param appId - The app to fetch incomingEditorState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ - public getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined { + public getIncomingEditorState( + appId: string, + removeAfterFetch?: boolean + ): EmbeddableEditorState | undefined { return this.getIncomingState( isEmbeddableEditorState, + appId, EMBEDDABLE_EDITOR_STATE_KEY, { keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_EDITOR_STATE_KEY] : undefined, @@ -64,24 +69,33 @@ export class EmbeddableStateTransfer { ); } - public clearEditorState() { + /** + * Clears the {@link EmbeddableEditorState | editor state} from the sessionStorage for the provided app id + * + * @param appId - The app to fetch incomingEditorState for + * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. + */ + public clearEditorState(appId: string) { const currentState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY); if (currentState) { - delete currentState[EMBEDDABLE_EDITOR_STATE_KEY]; + delete currentState[this.buildKey(appId, EMBEDDABLE_EDITOR_STATE_KEY)]; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, currentState); } } /** - * Fetches an {@link EmbeddablePackageState | embeddable package} argument from the sessionStorage + * Fetches an {@link EmbeddablePackageState | embeddable package} from the sessionStorage for the given AppId * + * @param appId - The app to fetch EmbeddablePackageState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ public getIncomingEmbeddablePackage( + appId: string, removeAfterFetch?: boolean ): EmbeddablePackageState | undefined { return this.getIncomingState( isEmbeddablePackageState, + appId, EMBEDDABLE_PACKAGE_STATE_KEY, { keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_PACKAGE_STATE_KEY] : undefined, @@ -122,20 +136,27 @@ export class EmbeddableStateTransfer { }); } + private buildKey(appId: string, key: string) { + return `${appId}-${key}`; + } + private getIncomingState( guard: (state: unknown) => state is IncomingStateType, + appId: string, key: string, options?: { keysToRemoveAfterFetch?: string[]; } ): IncomingStateType | undefined { - const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key]; + const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[ + this.buildKey(appId, key) + ]; const castState = !guard || guard(incomingState) ? (cloneDeep(incomingState) as IncomingStateType) : undefined; if (castState && options?.keysToRemoveAfterFetch) { const stateReplace = { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY) }; options.keysToRemoveAfterFetch.forEach((keyToRemove: string) => { - delete stateReplace[keyToRemove]; + delete stateReplace[this.buildKey(appId, keyToRemove)]; }); this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateReplace); } @@ -150,9 +171,9 @@ export class EmbeddableStateTransfer { const stateObject = options?.appendToExistingState ? { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY), - [key]: options.state, + [this.buildKey(appId, key)]: options.state, } - : { [key]: options?.state }; + : { [this.buildKey(appId, key)]: options?.state }; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject); await this.navigateToApp(appId, { path: options?.path }); } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 2f9b43121b45a..3e7014d54958d 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -590,11 +590,10 @@ export class EmbeddableStateTransfer { // Warning: (ae-forgotten-export) The symbol "ApplicationStart" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PublicAppInfo" needs to be exported by the entry point index.d.ts constructor(navigateToApp: ApplicationStart['navigateToApp'], currentAppId$: ApplicationStart['currentAppId$'], appList?: ReadonlyMap | undefined, customStorage?: Storage); - // (undocumented) - clearEditorState(): void; + clearEditorState(appId: string): void; getAppNameFromId: (appId: string) => string | undefined; - getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined; - getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined; + getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; + getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; // (undocumented) isTransferInProgress: boolean; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart" diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx index 6ca6efaa89797..fa0e0bd5f48f0 100644 --- a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -34,7 +34,7 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { useEffect(() => { const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } = - services.stateTransferService.getIncomingEditorState() || {}; + services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; setOriginatingApp(value); setValueInput(valueInputValue); setEmbeddableId(embeddableIdValue); diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index 7465e7eaa9044..c6333e978183f 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -22,6 +22,7 @@ import { import { VisualizeServices } from '../types'; import { VisualizeEditorCommon } from './visualize_editor_common'; import { VisualizeAppProps } from '../app'; +import { VisualizeConstants } from '../..'; export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); @@ -54,7 +55,8 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { useLinkedSearchUpdates(services, eventEmitter, appState, savedVisInstance); useEffect(() => { - const { originatingApp: value } = services.stateTransferService.getIncomingEditorState() || {}; + const { originatingApp: value } = + services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; setOriginatingApp(value); }, [services]); diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index c772554344cb2..bc766d63db5a7 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -65,7 +65,7 @@ export const VisualizeListing = () => { useMount(() => { // Reset editor state if the visualize listing page is loaded. - stateTransferService.clearEditorState(); + stateTransferService.clearEditorState(VisualizeConstants.APP_ID); chrome.setBreadcrumbs([ { text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 9ea42e8b56559..e8c3289d4ce41 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -142,7 +142,7 @@ export const getTopNavConfig = ( if (setOriginatingApp && originatingApp && newlyCreated) { setOriginatingApp(undefined); // remove editor state so the connection is still broken after reload - stateTransfer.clearEditorState(); + stateTransfer.clearEditorState(VisualizeConstants.APP_ID); } chrome.docTitle.change(savedVis.lastSavedTitle); chrome.setBreadcrumbs(getEditBreadcrumbs({}, savedVis.lastSavedTitle)); diff --git a/src/plugins/visualize/public/application/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts index 7dbf5be77b74d..6e901882a9365 100644 --- a/src/plugins/visualize/public/application/visualize_constants.ts +++ b/src/plugins/visualize/public/application/visualize_constants.ts @@ -16,4 +16,5 @@ export const VisualizeConstants = { CREATE_PATH: '/create', EDIT_PATH: '/edit', EDIT_BY_VALUE_PATH: '/edit_by_value', + APP_ID: 'visualize', }; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 3d82e6c60a1b6..4eb2d6fd2a731 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -132,7 +132,7 @@ export class VisualizePlugin setUISettings(core.uiSettings); core.application.register({ - id: 'visualize', + id: VisualizeConstants.APP_ID, title: 'Visualize', order: 8000, euiIconType: 'logoKibana', @@ -147,7 +147,9 @@ export class VisualizePlugin // allows the urlTracker to only save URLs that are not linked to an originatingApp this.isLinkedToOriginatingApp = () => { return Boolean( - pluginsStart.embeddable.getStateTransfer().getIncomingEditorState()?.originatingApp + pluginsStart.embeddable + .getStateTransfer() + .getIncomingEditorState(VisualizeConstants.APP_ID)?.originatingApp ); }; diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index 202b80d3d8406..c3e556b167889 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -6,6 +6,7 @@ */ export const PLUGIN_ID = 'lens'; +export const APP_ID = 'lens'; export const LENS_EMBEDDABLE_TYPE = 'lens'; export const DOC_TYPE = 'lens'; export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 7e95479887dbd..0d72a366fa411 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -38,7 +38,7 @@ import { SavedQuery, syncQueryStateWithUrl, } from '../../../../../src/plugins/data/public'; -import { LENS_EMBEDDABLE_TYPE, getFullPath } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common'; import { LensAppProps, LensAppServices, LensAppState } from './types'; import { getLensTopNavConfig } from './lens_top_nav'; import { Document } from '../persistence'; @@ -498,7 +498,7 @@ export function App({ isLinkedToOriginatingApp: false, })); // remove editor state so the connection is still broken after reload - stateTransfer.clearEditorState(); + stateTransfer.clearEditorState(APP_ID); redirectTo(newInput.savedObjectId); return; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 1ff31e5d4bf6b..5869151485a52 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -23,7 +23,7 @@ import { App } from './app'; import { EditorFrameStart } from '../types'; import { addHelpMenuToAppChrome } from '../help_menu_util'; import { LensPluginStartDependencies } from '../plugin'; -import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common'; import { LensEmbeddableInput, LensByReferenceInput, @@ -57,7 +57,7 @@ export async function mountApp( const storage = new Storage(localStorage); const stateTransfer = embeddable?.getStateTransfer(); const historyLocationState = params.history.location.state as HistoryLocationState; - const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(); + const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID); const lensServices: LensAppServices = { data, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 05da76d9fd207..c667ddea06b33 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -40,7 +40,7 @@ import { ACTION_VISUALIZE_FIELD, VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; -import { getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; @@ -182,7 +182,7 @@ export class LensPlugin { }; core.application.register({ - id: 'lens', + id: APP_ID, title: NOT_INTERNATIONALIZED_PRODUCT_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index ccd30126b67bd..4d1dff9303b0c 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -26,6 +26,7 @@ import { } from '../../../../src/plugins/kibana_utils/public'; import { ListPage, MapPage } from './routes'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; +import { APP_ID } from '../common/constants'; export let goToSpecifiedPath: (path: string) => void; export let kbnUrlStateStorage: IKbnUrlStateStorage; @@ -80,7 +81,7 @@ export async function renderApp({ function renderMapApp(routeProps: RouteComponentProps<{ savedMapId?: string }>) { const { embeddableId, originatingApp, valueInput } = - stateTransfer.getIncomingEditorState() || {}; + stateTransfer.getIncomingEditorState(APP_ID) || {}; let mapEmbeddableInput; if (routeProps.match.params.savedMapId) { diff --git a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx index 66b65eb8d0a9d..feafb34f6a715 100644 --- a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; import { getSavedObjectsClient, getToasts } from '../../kibana_services'; import { MapsListView } from './maps_list_view'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { APP_ID, MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { EmbeddableStateTransfer } from '../../../../../../src/plugins/embeddable/public'; export class LoadListAndRender extends React.Component<{ stateTransfer: EmbeddableStateTransfer }> { @@ -22,7 +22,7 @@ export class LoadListAndRender extends React.Component<{ stateTransfer: Embeddab componentDidMount() { this._isMounted = true; - this.props.stateTransfer.clearEditorState(); + this.props.stateTransfer.clearEditorState(APP_ID); this._loadMapsList(); } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index d38ff8b3e4da6..b6ee5274f690d 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; -import { MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +import { APP_ID, MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; import { createMapStore, MapStore, MapStoreState } from '../../../reducers/store'; import { getTimeFilters, @@ -364,7 +364,7 @@ export class SavedMap { this._originatingApp = undefined; // remove editor state so the connection is still broken after reload - this._getStateTransfer().clearEditorState(); + this._getStateTransfer().clearEditorState(APP_ID); getToasts().addSuccess({ title: i18n.translate('xpack.maps.topNav.saveSuccessMessage', { From cde3cbafe4f4506c7444b4c40a0a33027a4740e9 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 8 Feb 2021 15:53:01 -0500 Subject: [PATCH 024/100] fix summary alert details (#90657) --- .../factory/events/details/helpers.ts | 12 +- .../security_solution/timeline_details.ts | 158 +++++++++--------- 2 files changed, 82 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 4a6a1d61a9221..779454e9474ee 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -51,16 +51,8 @@ export const getDataFromSourceHits = ( { category: fieldCategory, field, - values: Array.isArray(item) - ? item.map((value) => { - if (isObject(value)) { - return JSON.stringify(value); - } - - return value; - }) - : [item], - originalValue: item, + values: toStringArray(item), + originalValue: toStringArray(item), } as TimelineEventsDetailsItem, ]; } else if (isObject(item)) { diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index 2705406009062..39b343a361945 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -19,97 +19,97 @@ const EXPECTED_DATA = [ category: 'base', field: '@timestamp', values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + originalValue: ['2019-02-10T02:39:44.107Z'], }, { category: '@version', field: '@version', values: ['1'], - originalValue: '1', + originalValue: ['1'], }, { category: 'agent', field: 'agent.ephemeral_id', values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], - originalValue: '909cd6a1-527d-41a5-9585-a7fb5386f851', + originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], }, { category: 'agent', field: 'agent.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'agent', field: 'agent.id', values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], - originalValue: '4d3ea604-27e5-4ec7-ab64-44f82285d776', + originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], }, { category: 'agent', field: 'agent.type', values: ['filebeat'], - originalValue: 'filebeat', + originalValue: ['filebeat'], }, { category: 'agent', field: 'agent.version', values: ['7.0.0'], - originalValue: '7.0.0', + originalValue: ['7.0.0'], }, { category: 'destination', field: 'destination.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { category: 'destination', field: 'destination.ip', values: ['10.100.7.196'], - originalValue: '10.100.7.196', + originalValue: ['10.100.7.196'], }, { category: 'destination', field: 'destination.port', - values: [40684], - originalValue: 40684, + values: ['40684'], + originalValue: ['40684'], }, { category: 'ecs', field: 'ecs.version', values: ['1.0.0-beta2'], - originalValue: '1.0.0-beta2', + originalValue: ['1.0.0-beta2'], }, { category: 'event', field: 'event.dataset', values: ['suricata.eve'], - originalValue: 'suricata.eve', + originalValue: ['suricata.eve'], }, { category: 'event', field: 'event.end', values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + originalValue: ['2019-02-10T02:39:44.107Z'], }, { category: 'event', field: 'event.kind', values: ['event'], - originalValue: 'event', + originalValue: ['event'], }, { category: 'event', field: 'event.module', values: ['suricata'], - originalValue: 'suricata', + originalValue: ['suricata'], }, { category: 'event', field: 'event.type', values: ['fileinfo'], - originalValue: 'fileinfo', + originalValue: ['fileinfo'], }, { category: 'file', @@ -117,260 +117,261 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'file', field: 'file.size', - values: [48277], - originalValue: 48277, + values: ['48277'], + originalValue: ['48277'], }, { category: 'fileset', field: 'fileset.name', values: ['eve'], - originalValue: 'eve', + originalValue: ['eve'], }, { category: 'flow', field: 'flow.locality', values: ['public'], - originalValue: 'public', + originalValue: ['public'], }, { category: 'host', field: 'host.architecture', values: ['armv7l'], - originalValue: 'armv7l', + originalValue: ['armv7l'], }, { category: 'host', field: 'host.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'host', field: 'host.id', values: ['b19a781f683541a7a25ee345133aa399'], - originalValue: 'b19a781f683541a7a25ee345133aa399', + originalValue: ['b19a781f683541a7a25ee345133aa399'], }, { category: 'host', field: 'host.name', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'host', field: 'host.os.codename', values: ['stretch'], - originalValue: 'stretch', + originalValue: ['stretch'], }, { category: 'host', field: 'host.os.family', values: [''], - originalValue: '', + originalValue: [''], }, { category: 'host', field: 'host.os.kernel', values: ['4.14.50-v7+'], - originalValue: '4.14.50-v7+', + originalValue: ['4.14.50-v7+'], }, { category: 'host', field: 'host.os.name', values: ['Raspbian GNU/Linux'], - originalValue: 'Raspbian GNU/Linux', + originalValue: ['Raspbian GNU/Linux'], }, { category: 'host', field: 'host.os.platform', values: ['raspbian'], - originalValue: 'raspbian', + originalValue: ['raspbian'], }, { category: 'host', field: 'host.os.version', values: ['9 (stretch)'], - originalValue: '9 (stretch)', + originalValue: ['9 (stretch)'], }, { category: 'http', field: 'http.request.method', values: ['get'], - originalValue: 'get', + originalValue: ['get'], }, { category: 'http', field: 'http.response.body.bytes', - values: [48277], - originalValue: 48277, + values: ['48277'], + originalValue: ['48277'], }, { category: 'http', field: 'http.response.status_code', - values: [206], - originalValue: 206, + values: ['206'], + originalValue: ['206'], }, { category: 'input', field: 'input.type', values: ['log'], - originalValue: 'log', + originalValue: ['log'], }, { category: 'base', field: 'labels.pipeline', values: ['filebeat-7.0.0-suricata-eve-pipeline'], - originalValue: 'filebeat-7.0.0-suricata-eve-pipeline', + originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'], }, { category: 'log', field: 'log.file.path', values: ['/var/log/suricata/eve.json'], - originalValue: '/var/log/suricata/eve.json', + originalValue: ['/var/log/suricata/eve.json'], }, { category: 'log', field: 'log.offset', - values: [1856288115], - originalValue: 1856288115, + values: ['1856288115'], + originalValue: ['1856288115'], }, { category: 'network', field: 'network.name', values: ['iot'], - originalValue: 'iot', + originalValue: ['iot'], }, { category: 'network', field: 'network.protocol', values: ['http'], - originalValue: 'http', + originalValue: ['http'], }, { category: 'network', field: 'network.transport', values: ['tcp'], - originalValue: 'tcp', + originalValue: ['tcp'], }, { category: 'service', field: 'service.type', values: ['suricata'], - originalValue: 'suricata', + originalValue: ['suricata'], }, { category: 'source', field: 'source.as.num', - values: [16509], - originalValue: 16509, + values: ['16509'], + originalValue: ['16509'], }, { category: 'source', field: 'source.as.org', values: ['Amazon.com, Inc.'], - originalValue: 'Amazon.com, Inc.', + originalValue: ['Amazon.com, Inc.'], }, { category: 'source', field: 'source.domain', values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], - originalValue: 'server-54-239-219-210.jfk51.r.cloudfront.net', + originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'], }, { category: 'source', field: 'source.geo.city_name', values: ['Seattle'], - originalValue: 'Seattle', + originalValue: ['Seattle'], }, { category: 'source', field: 'source.geo.continent_name', values: ['North America'], - originalValue: 'North America', + originalValue: ['North America'], }, { category: 'source', field: 'source.geo.country_iso_code', values: ['US'], - originalValue: 'US', + originalValue: ['US'], }, { category: 'source', field: 'source.geo.location.lat', - values: [47.6103], - originalValue: 47.6103, + values: ['47.6103'], + originalValue: ['47.6103'], }, { category: 'source', field: 'source.geo.location.lon', - values: [-122.3341], - originalValue: -122.3341, + values: ['-122.3341'], + originalValue: ['-122.3341'], }, { category: 'source', field: 'source.geo.region_iso_code', values: ['US-WA'], - originalValue: 'US-WA', + originalValue: ['US-WA'], }, { category: 'source', field: 'source.geo.region_name', values: ['Washington'], - originalValue: 'Washington', + originalValue: ['Washington'], }, { category: 'source', field: 'source.ip', values: ['54.239.219.210'], - originalValue: '54.239.219.210', + originalValue: ['54.239.219.210'], }, { category: 'source', field: 'source.port', - values: [80], - originalValue: 80, + values: ['80'], + originalValue: ['80'], }, { category: 'suricata', field: 'suricata.eve.fileinfo.state', values: ['CLOSED'], - originalValue: 'CLOSED', + originalValue: ['CLOSED'], }, { category: 'suricata', field: 'suricata.eve.fileinfo.tx_id', - values: [301], - originalValue: 301, + values: ['301'], + originalValue: ['301'], }, { category: 'suricata', field: 'suricata.eve.flow_id', - values: [196625917175466], - originalValue: 196625917175466, + values: ['196625917175466'], + originalValue: ['196625917175466'], }, { category: 'suricata', field: 'suricata.eve.http.http_content_type', values: ['video/mp4'], - originalValue: 'video/mp4', + originalValue: ['video/mp4'], }, { category: 'suricata', field: 'suricata.eve.http.protocol', values: ['HTTP/1.1'], - originalValue: 'HTTP/1.1', + originalValue: ['HTTP/1.1'], }, { category: 'suricata', field: 'suricata.eve.in_iface', values: ['eth0'], - originalValue: 'eth0', + originalValue: ['eth0'], }, { category: 'base', @@ -382,7 +383,7 @@ const EXPECTED_DATA = [ category: 'url', field: 'url.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { category: 'url', @@ -390,8 +391,9 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'url', @@ -399,26 +401,27 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: '_index', field: '_index', values: ['filebeat-7.0.0-iot-2019.06'], - originalValue: 'filebeat-7.0.0-iot-2019.06', + originalValue: ['filebeat-7.0.0-iot-2019.06'], }, { category: '_id', field: '_id', values: ['QRhG1WgBqd-n62SwZYDT'], - originalValue: 'QRhG1WgBqd-n62SwZYDT', + originalValue: ['QRhG1WgBqd-n62SwZYDT'], }, { category: '_score', field: '_score', - values: [1], - originalValue: 1, + values: ['1'], + originalValue: ['1'], }, ]; @@ -452,7 +455,6 @@ export default function ({ getService }: FtrProviderContext) { eventId: ID, }) .expect(200); - expect(sortBy(detailsData, 'name')).to.eql(sortBy(EXPECTED_DATA, 'name')); }); From 180f309fab1dec168a0a7ae81ac497f46966b15d Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Mon, 8 Feb 2021 16:05:26 -0500 Subject: [PATCH 025/100] Update security solution codeowners (#89038) * move the code coverage owner line for security solution next to the other lines about security solution * replace @elastic/siem with @elastic/security-solution and remove the duplicate code coverage owner for security_solution * remove elastic/endpoint-app-team and cleanup directories that no longer exist, and reorder lines * added case_api_integration code owners Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2917cc52a6c6d..b6c0c6afdee0b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -244,7 +244,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/security_api_integration/ @elastic/kibana-security /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security -#CC# /x-pack/plugins/security_solution/ @elastic/kibana-security #CC# /x-pack/plugins/security/ @elastic/kibana-security # Kibana Alerting Services @@ -312,25 +311,22 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib #CC# /x-pack/plugins/console_extensions/ @elastic/es-ui #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/es-ui -# Endpoint -/x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/security_solution_endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem -#CC# /x-pack/legacy/plugins/siem/ @elastic/siem -#CC# /x-pack/plugins/siem/ @elastic/siem -#CC# /x-pack/plugins/security_solution/ @elastic/siem - # Security Solution -/x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team -/x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/lists_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/api_integration/apis/security_solution @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team -#CC# /x-pack/plugins/security_solution/ @elastic/siem +/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/security-solution +/x-pack/test/security_solution_endpoint/ @elastic/security-solution +/x-pack/test/functional/es_archives/endpoint/ @elastic/security-solution +/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/security-solution +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution +/x-pack/plugins/security_solution/ @elastic/security-solution +/x-pack/test/detection_engine_api_integration @elastic/security-solution +/x-pack/test/lists_api_integration @elastic/security-solution +/x-pack/test/api_integration/apis/security_solution @elastic/security-solution +#CC# /x-pack/plugins/security_solution/ @elastic/security-solution + +# Security Solution sub teams +/x-pack/plugins/case @elastic/security-threat-hunting +/x-pack/test/case_api_integration @elastic/security-threat-hunting +/x-pack/plugins/lists @elastic/security-detections-response # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics @@ -362,3 +358,4 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Reporting #CC# /x-pack/plugins/reporting/ @elastic/kibana-reporting-services + From 3fa76956ac3e963cd6d89f2f390b47e47d3f65b0 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 8 Feb 2021 16:08:23 -0500 Subject: [PATCH 026/100] [CI] Automated backports via GitHub Actions - initial MVP (#90669) --- .github/CODEOWNERS | 1 + .github/workflows/backport.yml | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .github/workflows/backport.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b6c0c6afdee0b..ec07a6a03d2c8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -149,6 +149,7 @@ /src/cli/keystore/ @elastic/kibana-operations /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations +/.github/workflows/ @elastic/kibana-operations /vars/ @elastic/kibana-operations /.bazelignore @elastic/kibana-operations /.bazeliskversion @elastic/kibana-operations diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000000000..f64b9e95fbaab --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,46 @@ +on: + pull_request_target: + branches: + - master + types: + - labeled + - closed + +jobs: + backport: + name: Backport PR + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'auto-backport') + runs-on: ubuntu-latest + + steps: + - name: 'Get backport config' + run: | + curl 'https://raw.githubusercontent.com/elastic/kibana/master/.backportrc.json' > .backportrc.json + + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: Install backport CLI + run: npm install -g backport@5.6.4 + + - name: Backport PR + run: | + git config --global user.name "kibanamachine" + git config --global user.email "42973632+kibanamachine@users.noreply.github.com" + backport --fork true --username kibanamachine --accessToken "${{ secrets.KIBANAMACHINE_TOKEN }}" --ci --pr "$PR_NUMBER" --labels backport --assignee "$PR_OWNER" | tee 'output.log' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_OWNER: ${{ github.event.pull_request.user.login }} + + - name: Report backport status + run: | + COMMENT="Backport result + \`\`\` + $(cat output.log) + \`\`\`" + + GITHUB_TOKEN="${{ secrets.KIBANAMACHINE_TOKEN }}" gh api -X POST repos/elastic/kibana/issues/$PR_NUMBER/comments -F body="$COMMENT" + env: + PR_NUMBER: ${{ github.event.pull_request.number }} From 7a2b7550c962c5b6084f8f84e2dc9a2b5f3db0a2 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 8 Feb 2021 15:10:23 -0600 Subject: [PATCH 027/100] [DOCS] Fixes Dashboard formatting (#90485) * [DOCS] Fixes Dashboard formatting * Fixes the semi-structured search example * Update docs/user/dashboard/dashboard.asciidoc Co-authored-by: Wylie Conlon Co-authored-by: Wylie Conlon --- docs/user/dashboard/dashboard.asciidoc | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 8b3eddc008500..3c86c37f1fd30 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -133,15 +133,17 @@ image::dashboard/images/dashboard-filters.png[Labeled interface with semi-struct Semi-structured search:: Combine free text search with field-based search using the <>. Type a search term to match across all fields, or begin typing a field name to - get prompted with field names and operators you can use to build a structured query. - + + get prompted with field names and operators you can use to build a structured query. For example, in the sample web logs data, this query displays data only for the US: - . Enter `g`, and then select *geo.source*. - . Select *equals some value* and *US*, and then click *Update*. + . Enter `g`, then select *geo.source*. + . Select *equals some value* and *US*, then click *Update*. . For a more complex search, try: - `geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat"` +[source,text] +------------------- +geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat" +------------------- Time filter:: Dashboards have a global time filter that restricts the data that displays, but individual panels can @@ -152,21 +154,18 @@ Time filter:: . Open the panel menu, then select *More > Customize time range*. . On the *Customize panel time range* window, specify the new time range, then click *Add to panel*. - + [role="screenshot"] image:images/time_range_per_panel.gif[Time range per dashboard panel] Additional filters with AND:: - You can add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, using a basic editor or an advanced JSON editor for the {es} {ref}/query-dsl.html[query DSL]. - + Add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, using a basic editor or an advanced JSON editor for the {es} {ref}/query-dsl.html[query DSL]. When you use more than one index pattern on a dashboard, the filter editor allows you to filter only one dashboard. - To dynamically add filters, click a series on a dashboard. For example, to filter the dashboard to display only ios data: - . Click *Add filter*. . Set *Field* to *machine.os*, *Operator* to *is*, and *Value* to *ios*. . *Save* the filter. - . To remove the filter, click *x* next to the filter. + . To remove the filter, click *x*. [float] [[clone-panels]] From 46feb7659279b98d07ae7c7a259cd666605ba6ae Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 8 Feb 2021 23:42:07 +0200 Subject: [PATCH 028/100] [Alerts] Jira: Disallow labels with spaces (#90548) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/actions/README.md | 2 +- .../builtin_action_types/jira/schema.ts | 10 ++++++- .../builtin_action_types/jira/jira.test.tsx | 19 ++++++++++++- .../builtin_action_types/jira/jira.tsx | 7 +++++ .../builtin_action_types/jira/jira_params.tsx | 8 ++++++ .../builtin_action_types/jira/translations.ts | 7 +++++ .../actions/builtin_action_types/jira.ts | 28 +++++++++++++++++++ 7 files changed, 78 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 1eb94af4dddf8..1d50bc7e05807 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -657,7 +657,7 @@ The following table describes the properties of the `incident` object. | externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | | issueType | The id of the issue type in Jira. | string _(optional)_ | | priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. | string[] _(optional)_ | +| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | | parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ | #### `subActionParams (getIncident)` diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 552053bdd7651..a81dfaeef8175 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -40,7 +40,15 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ externalId: schema.nullable(schema.string()), issueType: schema.nullable(schema.string()), priority: schema.nullable(schema.string()), - labels: schema.nullable(schema.arrayOf(schema.string())), + labels: schema.nullable( + schema.arrayOf( + schema.string({ + validate: (label) => + // Matches any space, tab or newline character. + label.match(/\s/g) ? `The label ${label} cannot contain spaces` : undefined, + }) + ) + ), parent: schema.nullable(schema.string()), }), comments: schema.nullable( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 2d47740a581b8..ea1bcf82c314c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -96,7 +96,7 @@ describe('jira action params validation', () => { }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { 'subActionParams.incident.summary': [] }, + errors: { 'subActionParams.incident.summary': [], 'subActionParams.incident.labels': [] }, }); }); @@ -108,6 +108,23 @@ describe('jira action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.summary': ['Summary is required.'], + 'subActionParams.incident.labels': [], + }, + }); + }); + + test('params validation fails when labels contain spaces', () => { + const actionParams = { + subActionParams: { + incident: { summary: 'some title', labels: ['label with spaces'] }, + comments: [], + }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.summary': [], + 'subActionParams.incident.labels': ['Labels cannot contain spaces.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 5cb8a76d09bee..26b37278003c3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -72,6 +72,7 @@ export function getActionType(): ActionTypeModel => { const errors = { 'subActionParams.incident.summary': new Array(), + 'subActionParams.incident.labels': new Array(), }; const validationResult = { errors, @@ -83,6 +84,12 @@ export function getActionType(): ActionTypeModel label.match(/\s/g))) + errors['subActionParams.incident.labels'].push(i18n.LABELS_WHITE_SPACES); + } return validationResult; }, actionParamsFields: lazy(() => import('./jira_params')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 75930482797a2..cb2d637972cb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -184,6 +184,11 @@ const JiraParamsFields: React.FunctionComponent 0 && + incident.labels !== undefined; + return ( <> @@ -304,6 +309,8 @@ const JiraParamsFields: React.FunctionComponent
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 3c8bda7792f0a..fe7ea61e68193 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -199,3 +199,10 @@ export const SEARCH_ISSUES_LOADING = i18n.translate( defaultMessage: 'Loading...', } ); + +export const LABELS_WHITE_SPACES = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage', + { + defaultMessage: 'Labels cannot contain spaces.', + } +); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 6cc5e2eaefb94..8bd0b8a790d40 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -375,6 +375,34 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); + + it('should handle failing with a simulated success when labels containing a space', async () => { + await supertest + .post(`/api/actions/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + incident: { + ...mockJira.params.subActionParams.incident, + issueType: '10006', + labels: ['label with spaces'], + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.labels]: types that failed validation:\n - [subActionParams.incident.labels.0.0]: The label label with spaces cannot contain spaces\n - [subActionParams.incident.labels.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]', + }); + }); + }); }); describe('Execution', () => { From b39ad86b5d59fef96366affbdfbcc6758545cba9 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 8 Feb 2021 14:23:10 -0800 Subject: [PATCH 029/100] Use default ES distribution for functional tests (#88737) Signed-off-by: Tyler Smalley Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .ci/packer_cache_for_branch.sh | 1 - .../src/functional_test_runner/lib/config/schema.ts | 2 +- .../kbn-test/src/legacy_es/legacy_es_test_cluster.js | 2 +- .../migrationsv2/integration_tests/migration.test.ts | 2 +- .../migration_7.7.2_xpack_100k.test.ts | 2 +- src/core/test_helpers/kbn_server.ts | 4 ++-- src/dev/ci_setup/setup.sh | 2 -- test/api_integration/config.js | 5 ++++- test/common/config.js | 4 +--- test/common/services/deployment.ts | 12 +----------- test/examples/config.js | 5 ++++- test/functional/config.js | 6 ++++-- test/plugin_functional/config.ts | 5 ++++- test/server_integration/config.js | 5 ++++- test/server_integration/http/ssl/config.js | 5 ++++- test/server_integration/http/ssl_redirect/config.js | 5 ++++- test/server_integration/http/ssl_with_p12/config.js | 5 ++++- .../http/ssl_with_p12_intermediate/config.js | 5 ++++- 18 files changed, 44 insertions(+), 33 deletions(-) diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh index bbdf5484faf65..ee220537de340 100755 --- a/.ci/packer_cache_for_branch.sh +++ b/.ci/packer_cache_for_branch.sh @@ -26,7 +26,6 @@ source src/dev/ci_setup/setup.sh; # download es snapshots node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; # download reporting browsers (cd "x-pack" && node ../node_modules/.bin/gulp downloadChromium); 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 46471a4e9dac7..4fd28678d2653 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 @@ -169,7 +169,7 @@ export const schema = Joi.object() esTestCluster: Joi.object() .keys({ - license: Joi.string().default('oss'), + license: Joi.string().default('basic'), from: Joi.string().default('snapshot'), serverArgs: Joi.array(), serverEnvVars: Joi.object(), diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js index c04564279a971..43b6c90452b81 100644 --- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js +++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js @@ -22,7 +22,7 @@ export function createLegacyEsTestCluster(options = {}) { const { port = esTestConfig.getPort(), password = 'changeme', - license = 'oss', + license = 'basic', log, basePath = resolve(KIBANA_ROOT, '.es'), esFrom = esTestConfig.getBuildFrom(), diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index 2d3ab91697e42..317bfe33b3a19 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -30,7 +30,7 @@ describe('migration v2', () => { adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { - license: oss ? 'oss' : 'trial', + license: 'trial', dataArchive, }, }, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts index bce01c93fe886..16ba0c855867c 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts @@ -32,7 +32,7 @@ describe.skip('migration from 7.7.2-xpack with 100k objects', () => { adjustTimeout: (t: number) => jest.setTimeout(600000), settings: { es: { - license: oss ? 'oss' : 'trial', + license: 'trial', dataArchive, }, }, diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 011ba67a05512..14f614643ac9f 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -185,7 +185,7 @@ export function createTestServers({ adjustTimeout: (timeout: number) => void; settings?: { es?: { - license: 'oss' | 'basic' | 'gold' | 'trial'; + license: 'basic' | 'gold' | 'trial'; [key: string]: any; }; kbn?: { @@ -208,7 +208,7 @@ export function createTestServers({ if (!adjustTimeout) { throw new Error('adjustTimeout is required in order to avoid flaky tests'); } - const license = get(settings, 'es.license', 'oss'); + const license = get(settings, 'es.license', 'basic'); const usersToBeAdded = get(settings, 'users', []); if (usersToBeAdded.length > 0) { if (license !== 'trial') { diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 0b24f0b22b81a..db7110d2d0875 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -32,8 +32,6 @@ yarn kbn bootstrap ### echo " -- downloading es snapshot" node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; - ### ### verify no git modifications diff --git a/test/api_integration/config.js b/test/api_integration/config.js index d688c31dc47e7..bd8f10606a45a 100644 --- a/test/api_integration/config.js +++ b/test/api_integration/config.js @@ -19,7 +19,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'API Integration Tests', }, - esTestCluster: commonConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/common/config.js b/test/common/config.js index 9d108f05fd1fc..46cd07b2ec370 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -21,9 +21,7 @@ export default function () { servers, esTestCluster: { - license: 'oss', - from: 'snapshot', - serverArgs: [], + serverArgs: ['xpack.security.enabled=false'], }, kbnTestServer: { diff --git a/test/common/services/deployment.ts b/test/common/services/deployment.ts index a19118bb3065a..510124ce3d1b7 100644 --- a/test/common/services/deployment.ts +++ b/test/common/services/deployment.ts @@ -35,17 +35,7 @@ export function DeploymentProvider({ getService }: FtrProviderContext) { * Useful for functional testing in cloud environment */ async isOss() { - const baseUrl = this.getEsHostPort(); - const username = config.get('servers.elasticsearch.username'); - const password = config.get('servers.elasticsearch.password'); - const response = await fetch(baseUrl + '/_xpack', { - method: 'get', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'), - }, - }); - return response.status !== 200; + return config.get('kbnTestServer.serverArgs').indexOf('--oss') > -1; }, async isCloud(): Promise { diff --git a/test/examples/config.js b/test/examples/config.js index fd1ad671cf4bf..0ba7af0bfceb7 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -34,7 +34,10 @@ export default async function ({ readConfigFile }) { }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, apps: functionalConfig.get('apps'), esArchiver: { directory: path.resolve(__dirname, '../es_archives'), diff --git a/test/functional/config.js b/test/functional/config.js index c15cfffbdb576..05d6cf9dd6b68 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -32,8 +32,10 @@ export default async function ({ readConfigFile }) { servers: commonConfig.get('servers'), - esTestCluster: commonConfig.get('esTestCluster'), - + esTestCluster: { + ...commonConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...commonConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index f28e219884bde..bd5ef814ae6c0 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -36,7 +36,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, apps: functionalConfig.get('apps'), esArchiver: { directory: path.resolve(__dirname, '../es_archives'), diff --git a/test/server_integration/config.js b/test/server_integration/config.js index 7171a9b33bfd8..0ebb5c48033b8 100644 --- a/test/server_integration/config.js +++ b/test/server_integration/config.js @@ -27,7 +27,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Integration Tests', }, - esTestCluster: commonConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index b305728b64de2..14381de6667fd 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index 0c3e8ce78237a..d19883bcfe241 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -44,7 +44,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js index 75a33226aa669..c4621500e927d 100644 --- a/test/server_integration/http/ssl_with_p12/config.js +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_with_p12_intermediate/config.js b/test/server_integration/http/ssl_with_p12_intermediate/config.js index a120ea0b3a556..7f32bad648351 100644 --- a/test/server_integration/http/ssl_with_p12_intermediate/config.js +++ b/test/server_integration/http/ssl_with_p12_intermediate/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ From 15a4c285b860904b6546519ba3905b85a260554a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 Feb 2021 22:24:14 +0000 Subject: [PATCH 030/100] chore(NA): move bazel workspace status from bash script into nodejs executable (#90560) * chore(NA): move bazel workspace status into nodejs executable * chore(NA): removed unused console.log on error * chore(NA): ability to setup different name for origin remote on workspace status command * chore(NA): do not fail if cant collect repo url Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .bazelrc | 2 +- src/dev/bazel_workspace_status.js | 80 +++++++++++++++++++++++++++++++ src/dev/bazel_workspace_status.sh | 57 ---------------------- 3 files changed, 81 insertions(+), 58 deletions(-) create mode 100644 src/dev/bazel_workspace_status.js delete mode 100755 src/dev/bazel_workspace_status.sh diff --git a/.bazelrc b/.bazelrc index 158338ec5f093..5fa6ef245fcea 100644 --- a/.bazelrc +++ b/.bazelrc @@ -11,7 +11,7 @@ import %workspace%/.bazelrc.common # BuildBuddy ## Metadata settings -build --workspace_status_command=$(pwd)/src/dev/bazel_workspace_status.sh +build --workspace_status_command="node ./src/dev/bazel_workspace_status.js" # Enable this in case you want to share your build info # build --build_metadata=VISIBILITY=PUBLIC build --build_metadata=TEST_GROUPS=//packages diff --git a/src/dev/bazel_workspace_status.js b/src/dev/bazel_workspace_status.js new file mode 100644 index 0000000000000..fe60f9176d243 --- /dev/null +++ b/src/dev/bazel_workspace_status.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh +// This script will be run bazel when building process starts to +// generate key-value information that represents the status of the +// workspace. The output should be like +// +// KEY1 VALUE1 +// KEY2 VALUE2 +// +// If the script exits with non-zero code, it's considered as a failure +// and the output will be discarded. + +(async () => { + const execa = require('execa'); + const os = require('os'); + + async function runCmd(cmd, args) { + try { + return await execa(cmd, args); + } catch (e) { + return { exitCode: 1 }; + } + } + + // Git repo + const kbnGitOriginName = process.env.KBN_GIT_ORIGIN_NAME || 'origin'; + const repoUrlCmdResult = await runCmd('git', [ + 'config', + '--get', + `remote.${kbnGitOriginName}.url`, + ]); + if (repoUrlCmdResult.exitCode === 0) { + // Only output REPO_URL when found it + console.log(`REPO_URL ${repoUrlCmdResult.stdout}`); + } + + // Commit SHA + const commitSHACmdResult = await runCmd('git', ['rev-parse', 'HEAD']); + if (commitSHACmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`COMMIT_SHA ${commitSHACmdResult.stdout}`); + + // Git branch + const gitBranchCmdResult = await runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']); + if (gitBranchCmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`GIT_BRANCH ${gitBranchCmdResult.stdout}`); + + // Tree status + const treeStatusCmdResult = await runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']); + const treeStatusVarStr = 'GIT_TREE_STATUS'; + if (treeStatusCmdResult.exitCode === 0) { + console.log(`${treeStatusVarStr} Clean`); + } else { + console.log(`${treeStatusVarStr} Modified`); + } + + // Host + if (process.env.CI) { + const hostCmdResult = await runCmd('hostname'); + const hostStr = hostCmdResult.stdout.split('-').slice(0, -1).join('-'); + const coresStr = os.cpus().filter((cpu, index) => { + return !cpu.model.includes('Intel') || index % 2 === 1; + }).length; + + if (hostCmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`HOST ${hostStr}-${coresStr}`); + } +})(); diff --git a/src/dev/bazel_workspace_status.sh b/src/dev/bazel_workspace_status.sh deleted file mode 100755 index efaca4bb98849..0000000000000 --- a/src/dev/bazel_workspace_status.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh -# This script will be run bazel when building process starts to -# generate key-value information that represents the status of the -# workspace. The output should be like -# -# KEY1 VALUE1 -# KEY2 VALUE2 -# -# If the script exits with non-zero code, it's considered as a failure -# and the output will be discarded. - -# Git repo -repo_url=$(git config --get remote.origin.url) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "REPO_URL ${repo_url}" - -# Commit SHA -commit_sha=$(git rev-parse HEAD) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "COMMIT_SHA ${commit_sha}" - -# Git branch -repo_url=$(git rev-parse --abbrev-ref HEAD) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "GIT_BRANCH ${repo_url}" - -# Tree status -git diff-index --quiet HEAD -- -if [[ $? == 0 ]]; -then - tree_status="Clean" -else - tree_status="Modified" -fi -echo "GIT_TREE_STATUS ${tree_status}" - -# Host -if [ "$CI" = "true" ]; then - host=$(hostname | sed 's|\(.*\)-.*|\1|') - cores=$(grep ^cpu\\scores /proc/cpuinfo | uniq | awk '{print $4}' ) - if [[ $? != 0 ]]; - then - exit 1 - fi - echo "HOST ${host}-${cores}" -fi From 54863889d496361239dea50360b68f390160e03c Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 Feb 2021 22:25:05 +0000 Subject: [PATCH 031/100] chore(NA): remove write permissions on Bazel remote cache for PRs (#90652) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/ci_setup/setup_env.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 0b835d4b9fa94..2deafaaf35a94 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -180,6 +180,15 @@ fi ### cp -f "$KIBANA_DIR/src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; +### +### remove write permissions on buildbuddy remote cache for prs +### +if [[ "$ghprbPullId" ]] ; then + echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" + echo "# Uploads logs & artifacts without writing to cache" >> "$HOME/.bazelrc" + echo "build --noremote_upload_local_results" >> "$HOME/.bazelrc" +fi + ### ### append auth token to buildbuddy into "$HOME/.bazelrc"; ### From 3ef3e9324fd9d77dd442b09e3355fb005c4b3caf Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 8 Feb 2021 14:46:57 -0800 Subject: [PATCH 032/100] [dev-utils/ship-ci-stats] fail when CI stats is down (#90678) Co-authored-by: spalger --- .../src/ci_stats_reporter/ci_stats_reporter.ts | 2 +- .../src/ci_stats_reporter/ship_ci_stats_cli.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index cb5175142c160..93826cf3add80 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -109,7 +109,7 @@ export class CiStatsReporter { }, }); - return; + return true; } catch (error) { if (!error?.request) { // not an axios error, must be a usage error that we should notify user about diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts index 244af7b657418..1ee78518bb801 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import { CiStatsReporter } from './ci_stats_reporter'; -import { run, createFlagError } from '../run'; +import { run, createFlagError, createFailError } from '../run'; export function shipCiStatsCli() { run( @@ -23,12 +23,20 @@ export function shipCiStatsCli() { } const reporter = CiStatsReporter.fromEnv(log); + + if (!reporter.isEnabled()) { + throw createFailError('unable to initilize the CI Stats reporter'); + } + for (const path of metricPaths) { // resolve path from CLI relative to CWD const abs = Path.resolve(path); const json = Fs.readFileSync(abs, 'utf8'); - await reporter.metrics(JSON.parse(json)); - log.success('shipped metrics from', path); + if (await reporter.metrics(JSON.parse(json))) { + log.success('shipped metrics from', path); + } else { + throw createFailError('failed to ship metrics'); + } } }, { From 3cf00d2bb40c43471327501752ce099ce7f36a21 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 8 Feb 2021 15:21:06 -0800 Subject: [PATCH 033/100] [Time to Visualize] Adds functional tests for linking/unlinking panel from embeddable library (#89612) --- .github/CODEOWNERS | 1 + .../actions/add_to_library_action.tsx | 2 +- .../apps/dashboard/embeddable_library.ts | 111 ++++++++++++++++++ test/functional/apps/dashboard/index.ts | 1 + .../services/dashboard/panel_actions.ts | 25 ++++ x-pack/test/functional/apps/lens/dashboard.ts | 38 ++++++ .../maps/embeddable/embeddable_library.js | 80 +++++++++++++ .../functional/apps/maps/embeddable/index.js | 1 + 8 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 test/functional/apps/dashboard/embeddable_library.ts create mode 100644 x-pack/test/functional/apps/maps/embeddable/embeddable_library.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ec07a6a03d2c8..34b449346ddf7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -91,6 +91,7 @@ /src/plugins/dashboard/ @elastic/kibana-presentation /src/plugins/input_control_vis/ @elastic/kibana-presentation /src/plugins/vis_type_markdown/ @elastic/kibana-presentation +/test/functional/apps/dashboard/ @elastic/kibana-presentation /x-pack/plugins/canvas/ @elastic/kibana-presentation /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index 5d384ed8ebd82..ef730e16bc5cf 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -22,7 +22,7 @@ import { NotificationsStart } from '../../services/core'; import { dashboardAddToLibraryAction } from '../../dashboard_strings'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; -export const ACTION_ADD_TO_LIBRARY = 'addToFromLibrary'; +export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary'; export interface AddToLibraryActionContext { embeddable: IEmbeddable; diff --git a/test/functional/apps/dashboard/embeddable_library.ts b/test/functional/apps/dashboard/embeddable_library.ts new file mode 100644 index 0000000000000..20fe9aeb1387a --- /dev/null +++ b/test/functional/apps/dashboard/embeddable_library.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); + const esArchiver = getService('esArchiver'); + const find = getService('find'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const panelActions = getService('dashboardPanelActions'); + + describe('embeddable library', () => { + before(async () => { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('unlink visualize panel from embeddable library', async () => { + // add heatmap panel from library + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: heatmap'); + await find.clickByButtonText('Rendering Test: heatmap'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: heatmap'); + await find.existsByLinkText('Rendering Test: heatmap'); + await dashboardAddPanel.closeAddPanel(); + }); + + it('save visualize panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + await panelActions.saveToLibrary('Rendering Test: heatmap - copy', originalPanel); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find( + 'embeddablePanelHeading-RenderingTest:heatmap-copy' + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + }); + + it('unlink map panel from embeddable library', async () => { + // add map panel from library + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); + await find.clickByButtonText('Rendering Test: geo map'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); + await find.existsByLinkText('Rendering Test: geo map'); + await dashboardAddPanel.closeAddPanel(); + }); + + it('save map panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + await panelActions.saveToLibrary('Rendering Test: geo map - copy', originalPanel); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find( + 'embeddablePanelHeading-RenderingTest:geomap-copy' + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 9332503539874..b71a89501fbf6 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -81,6 +81,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. // If we don't use the timestamp in the URL, the colors in the charts will be different. loadTestFile(require.resolve('./dashboard_snapshots')); + loadTestFile(require.resolve('./embeddable_library')); }); // Each of these tests call initTests themselves, the way it was originally written. The above tests only load diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 534d4cebd92f4..881e3ad4157a4 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -17,6 +17,8 @@ const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel'; const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'; const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector'; +const LIBRARY_NOTIFICATION_TEST_SUBJ = 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION'; +const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary'; export function DashboardPanelActionsProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); @@ -170,6 +172,29 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ); } + async unlinkFromLibary(parent?: WebElementWrapper) { + log.debug('unlinkFromLibrary'); + const libraryNotification = parent + ? await testSubjects.findDescendant(LIBRARY_NOTIFICATION_TEST_SUBJ, parent) + : await testSubjects.find(LIBRARY_NOTIFICATION_TEST_SUBJ); + await libraryNotification.click(); + await testSubjects.click('libraryNotificationUnlinkButton'); + } + + async saveToLibrary(newTitle: string, parent?: WebElementWrapper) { + log.debug('saveToLibrary'); + await this.openContextMenu(parent); + const exists = await testSubjects.exists(SAVE_TO_LIBRARY_TEST_SUBJ); + if (!exists) { + await this.clickContextMenuMoreItem(); + } + await testSubjects.click(SAVE_TO_LIBRARY_TEST_SUBJ); + await testSubjects.setValue('savedObjectTitle', newTitle, { + clearWithKeyboard: true, + }); + await testSubjects.click('confirmSaveSavedObjectButton'); + } + async expectExistsRemovePanelAction() { log.debug('expectExistsRemovePanelAction'); await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 738e45c1cbcf1..5cbd5dff45e1e 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -156,5 +156,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await panelActions.clickContextMenuMoreItem(); await testSubjects.existOrFail(ACTION_TEST_SUBJ); }); + + it('unlink lens panel from embeddable library', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.clickByButtonText('lnsPieVis'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + }); + + it('save lens panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + await panelActions.saveToLibrary('lnsPieVis - copy', originalPanel); + await testSubjects.click('confirmSaveSavedObjectButton'); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis-copy'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.existsByLinkText('lnsPieVis'); + }); }); } diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js new file mode 100644 index 0000000000000..40e73f0d8a763 --- /dev/null +++ b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'maps', 'visualize']); + const kibanaServer = getService('kibanaServer'); + const security = getService('security'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardVisualizations = getService('dashboardVisualizations'); + + describe('maps in embeddable library', () => { + before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + ], + false + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickCreateNewLink(); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMapsApp(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + await PageObjects.maps.clickSaveAndReturnButton(); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('save map panel to embeddable library', async () => { + await dashboardPanelActions.saveToLibrary('embeddable library map'); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const mapPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + mapPanel + ); + expect(libraryActionExists).to.be(true); + }); + + it('unlink map panel from embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + await dashboardPanelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('embeddable library map'); + await find.existsByLinkText('embeddable library map'); + await dashboardAddPanel.closeAddPanel(); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/embeddable/index.js index 815de2e081309..9fd4c9db703db 100644 --- a/x-pack/test/functional/apps/maps/embeddable/index.js +++ b/x-pack/test/functional/apps/maps/embeddable/index.js @@ -9,6 +9,7 @@ export default function ({ loadTestFile }) { describe('embeddable', function () { loadTestFile(require.resolve('./save_and_return')); loadTestFile(require.resolve('./dashboard')); + loadTestFile(require.resolve('./embeddable_library')); loadTestFile(require.resolve('./embeddable_state')); loadTestFile(require.resolve('./tooltip_filter_actions')); }); From 87212e68f71dab0b827cecc78d8e60c9eb821298 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Mon, 8 Feb 2021 19:08:15 -0500 Subject: [PATCH 034/100] [Upgrade Assistant] Add A11y Tests (#90265) * Added overview a11y test. Added data test subjects to the tabs in the app. * Added data test subjects for all tabs and the detail panel on the indidual pages. Updated tests to wait for detail panel to be visible before taking snapshot. * Updated snapshot for upgrade assistant jest tests. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/components/tabs.tsx | 3 ++ .../__snapshots__/checkup_tab.test.tsx.snap | 12 +++-- .../components/tabs/checkup/checkup_tab.tsx | 6 ++- .../components/tabs/overview/index.tsx | 2 +- .../accessibility/apps/upgrade_assistant.ts | 44 +++++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + 6 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/accessibility/apps/upgrade_assistant.ts diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index d77349e53b354..fa6badb34635b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -190,6 +190,7 @@ export class UpgradeAssistantTabs extends React.Component { return [ { id: 'overview', + 'data-test-subj': 'upgradeAssistantOverviewTab', name: i18n.translate('xpack.upgradeAssistant.overviewTab.overviewTabTitle', { defaultMessage: 'Overview', }), @@ -197,6 +198,7 @@ export class UpgradeAssistantTabs extends React.Component { }, { id: 'cluster', + 'data-test-subj': 'upgradeAssistantClusterTab', name: i18n.translate('xpack.upgradeAssistant.checkupTab.clusterTabLabel', { defaultMessage: 'Cluster', }), @@ -213,6 +215,7 @@ export class UpgradeAssistantTabs extends React.Component { }, { id: 'indices', + 'data-test-subj': 'upgradeAssistantIndicesTab', name: i18n.translate('xpack.upgradeAssistant.checkupTab.indicesTabLabel', { defaultMessage: 'Indices', }), diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap index 5aa4a469e4f02..bac67bf722ea7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap @@ -6,7 +6,9 @@ exports[`CheckupTab render with deprecations 1`] = ` -

+

-

+

-

+

= ({ <> -

+

= (props) <> - +

{ + before(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + }); + + it('Overview Tab', async () => { + await retry.waitFor('Upgrade Assistant overview tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantOverviewTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + + it('Cluster Tab', async () => { + await testSubjects.click('upgradeAssistantClusterTab'); + await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantClusterTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + + it('Indices Tab', async () => { + await testSubjects.click('upgradeAssistantIndicesTab'); + await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantIndexTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 94a09e3f767f6..24c46c1a1687e 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -31,6 +31,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/index_lifecycle_management'), require.resolve('./apps/ml'), require.resolve('./apps/lens'), + require.resolve('./apps/upgrade_assistant'), ], pageObjects, From 231610c7204ac5df84083cb39ab5d4c860a2ef5c Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 8 Feb 2021 21:50:07 -0500 Subject: [PATCH 035/100] [Monitoring] Migrate data source for legacy alerts to monitoring data directly (#87377) * License expiration * Fetch legacy alert data from the source * Add back in the one test file * Remove deprecated code * Fix up tests * Add test files * Fix i18n * Update tests * PR feedback * Fix types and tests * Fix license headers * Remove unused function * Fix faulty license expiration logic --- .../plugins/monitoring/common/types/alerts.ts | 51 ++++- x-pack/plugins/monitoring/common/types/es.ts | 6 +- .../monitoring/server/alerts/base_alert.ts | 88 +------- .../alerts/cluster_health_alert.test.ts | 50 +++-- .../server/alerts/cluster_health_alert.ts | 147 ++++++++----- ...asticsearch_version_mismatch_alert.test.ts | 58 +++-- .../elasticsearch_version_mismatch_alert.ts | 152 ++++++++----- .../kibana_version_mismatch_alert.test.ts | 57 +++-- .../alerts/kibana_version_mismatch_alert.ts | 157 ++++++++----- .../alerts/license_expiration_alert.test.ts | 102 ++++++--- .../server/alerts/license_expiration_alert.ts | 159 +++++++++----- .../logstash_version_mismatch_alert.test.ts | 57 +++-- .../alerts/logstash_version_mismatch_alert.ts | 152 ++++++++----- .../server/alerts/nodes_changed_alert.test.ts | 97 +++++++-- .../server/alerts/nodes_changed_alert.ts | 206 ++++++++++++------ .../lib/alerts/fetch_cluster_health.test.ts | 42 ++++ .../server/lib/alerts/fetch_cluster_health.ts | 69 ++++++ .../fetch_elasticsearch_versions.test.ts | 52 +++++ .../alerts/fetch_elasticsearch_versions.ts | 71 ++++++ .../lib/alerts/fetch_kibana_versions.test.ts | 74 +++++++ .../lib/alerts/fetch_kibana_versions.ts | 111 ++++++++++ .../lib/alerts/fetch_legacy_alerts.test.ts | 96 -------- .../server/lib/alerts/fetch_legacy_alerts.ts | 97 --------- .../server/lib/alerts/fetch_licenses.test.ts | 61 ++++++ .../server/lib/alerts/fetch_licenses.ts | 75 +++++++ .../alerts/fetch_logstash_versions.test.ts | 74 +++++++ .../lib/alerts/fetch_logstash_versions.ts | 111 ++++++++++ .../alerts/fetch_nodes_from_cluster_stats.ts | 105 +++++++++ .../server/lib/alerts/fetch_status.ts | 2 +- .../server/routes/api/v1/alerts/enable.ts | 3 +- .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - 32 files changed, 1802 insertions(+), 792 deletions(-) create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 7fb41ece527a1..649b92cb7ac82 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -6,7 +6,12 @@ */ import { Alert, AlertTypeParams, SanitizedAlert } from '../../../alerts/common'; -import { AlertParamType, AlertMessageTokenType, AlertSeverity } from '../enums'; +import { + AlertParamType, + AlertMessageTokenType, + AlertSeverity, + AlertClusterHealthType, +} from '../enums'; export type CommonAlert = Alert | SanitizedAlert; @@ -60,6 +65,8 @@ export interface AlertInstanceState { | AlertDiskUsageState | AlertThreadPoolRejectionsState | AlertNodeState + | AlertLicenseState + | AlertNodesChangedState >; [x: string]: unknown; } @@ -74,6 +81,7 @@ export interface AlertState { export interface AlertNodeState extends AlertState { nodeId: string; nodeName?: string; + meta: any; [key: string]: unknown; } @@ -96,6 +104,14 @@ export interface AlertThreadPoolRejectionsState extends AlertState { nodeName?: string; } +export interface AlertLicenseState extends AlertState { + expiryDateMS: number; +} + +export interface AlertNodesChangedState extends AlertState { + node: AlertClusterStatsNode; +} + export interface AlertUiState { isFiring: boolean; resolvedMS?: number; @@ -228,3 +244,36 @@ export interface LegacyAlertNodesChangedList { added: { [nodeName: string]: string }; restarted: { [nodeName: string]: string }; } + +export interface AlertLicense { + status: string; + type: string; + expiryDateMS: number; + clusterUuid: string; + ccs?: string; +} + +export interface AlertClusterStatsNodes { + clusterUuid: string; + recentNodes: AlertClusterStatsNode[]; + priorNodes: AlertClusterStatsNode[]; + ccs?: string; +} + +export interface AlertClusterStatsNode { + nodeUuid: string; + nodeEphemeralId?: string; + nodeName?: string; +} + +export interface AlertClusterHealth { + health: AlertClusterHealthType; + clusterUuid: string; + ccs?: string; +} + +export interface AlertVersions { + clusterUuid: string; + ccs?: string; + versions: string[]; +} diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index cb3d44d0080ed..9dce32211f4b1 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -154,7 +154,10 @@ export interface ElasticsearchLegacySource { cluster_state?: { status?: string; nodes?: { - [nodeUuid: string]: {}; + [nodeUuid: string]: { + ephemeral_id?: string; + name?: string; + }; }; master_node?: boolean; }; @@ -170,6 +173,7 @@ export interface ElasticsearchLegacySource { license?: { status?: string; type?: string; + expiry_date_in_millis?: number; }; logstash_state?: { pipeline?: { diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 200c61b29b2e0..e79eb78f7f66b 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -26,26 +26,16 @@ import { AlertEnableAction, CommonAlertFilter, CommonAlertParams, - LegacyAlert, } from '../../common/types/alerts'; import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; -import { MonitoringLicenseService } from '../types'; import { mbSafeQuery } from '../lib/mb_safe_query'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerts/common/parse_duration'; import { Globals } from '../static_globals'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; -import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; - -interface LegacyOptions { - watchName: string; - nodeNameLabel: string; - changeDataValues?: Partial; -} type ExecutedState = | { @@ -60,7 +50,6 @@ interface AlertOptions { name: string; throttle?: string | null; interval?: string; - legacy?: LegacyOptions; defaultParams?: Partial; actionVariables: Array<{ name: string; description: string }>; fetchClustersRange?: number; @@ -126,16 +115,6 @@ export class BaseAlert { }; } - public isEnabled(licenseService: MonitoringLicenseService) { - if (this.alertOptions.legacy) { - const watcherFeature = licenseService.getWatcherFeature(); - if (!watcherFeature.isAvailable || !watcherFeature.isEnabled) { - return false; - } - } - return true; - } - public getId() { return this.rawAlert?.id; } @@ -271,10 +250,6 @@ export class BaseAlert { params as CommonAlertParams, availableCcs ); - if (this.alertOptions.legacy) { - const data = await this.fetchLegacyData(callCluster, clusters, availableCcs); - return await this.processLegacyData(data, clusters, services, state); - } const data = await this.fetchData(params, callCluster, clusters, availableCcs); return await this.processData(data, clusters, services, state); } @@ -312,35 +287,6 @@ export class BaseAlert { throw new Error('Child classes must implement `fetchData`'); } - protected async fetchLegacyData( - callCluster: CallCluster, - clusters: AlertCluster[], - availableCcs: string[] - ): Promise { - let alertIndexPattern = INDEX_ALERTS; - if (availableCcs) { - alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); - } - const legacyAlerts = await fetchLegacyAlerts( - callCluster, - clusters, - alertIndexPattern, - this.alertOptions.legacy!.watchName, - Globals.app.config.ui.max_bucket_size - ); - - return legacyAlerts.map((legacyAlert) => { - return { - clusterUuid: legacyAlert.metadata.cluster_uuid, - shouldFire: !legacyAlert.resolved_timestamp, - severity: mapLegacySeverity(legacyAlert.metadata.severity), - meta: legacyAlert, - nodeName: this.alertOptions.legacy!.nodeNameLabel, - ...this.alertOptions.legacy!.changeDataValues, - }; - }); - } - protected async processData( data: AlertData[], clusters: AlertCluster[], @@ -395,34 +341,6 @@ export class BaseAlert { return state; } - protected async processLegacyData( - data: AlertData[], - clusters: AlertCluster[], - services: AlertServices, - state: ExecutedState - ) { - const currentUTC = +new Date(); - for (const item of data) { - const instanceId = `${this.alertOptions.id}:${item.clusterUuid}`; - const instance = services.alertInstanceFactory(instanceId); - if (!item.shouldFire) { - instance.replaceState({ alertStates: [] }); - continue; - } - const cluster = clusters.find((c: AlertCluster) => c.clusterUuid === item.clusterUuid); - const alertState: AlertState = this.getDefaultAlertState(cluster!, item); - alertState.nodeName = item.nodeName; - alertState.ui.triggeredMS = currentUTC; - alertState.ui.isFiring = true; - alertState.ui.severity = item.severity; - alertState.ui.message = this.getUiMessage(alertState, item); - instance.replaceState({ alertStates: [alertState] }); - this.executeActions(instance, alertState, item, cluster); - } - state.lastChecked = currentUTC; - return state; - } - protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState { return { cluster, @@ -437,10 +355,6 @@ export class BaseAlert { }; } - protected getVersions(legacyAlert: LegacyAlert) { - return `[${legacyAlert.message.match(/(?<=Versions: \[).+?(?=\])/)}]`; - } - protected getUiMessage( alertState: AlertState | unknown, item: AlertData | unknown diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index 3d8000d317526..1490a6ce58e04 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -7,7 +7,8 @@ import { ClusterHealthAlert } from './cluster_health_alert'; import { ALERT_CLUSTER_HEALTH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { AlertClusterHealthType, AlertSeverity } from '../../common/enums'; +import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; @@ -26,8 +27,8 @@ jest.mock('../static_globals', () => ({ }, })); -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_cluster_health', () => ({ + fetchClusterHealth: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -63,16 +64,16 @@ describe('ClusterHealthAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'Elasticsearch cluster status is yellow.', - message: 'Allocate missing replica shards.', - metadata: { - severity: 2000, - cluster_uuid: clusterUuid, + const healths = [ + { + health: AlertClusterHealthType.Yellow, + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -94,8 +95,8 @@ describe('ClusterHealthAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchClusterHealth as jest.Mock).mockImplementation(() => { + return healths; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -120,8 +121,15 @@ describe('ClusterHealthAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Elasticsearch cluster alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + health: AlertClusterHealthType.Yellow, + }, ui: { isFiring: true, message: { @@ -140,7 +148,7 @@ describe('ClusterHealthAlert', () => { }, ], }, - severity: 'danger', + severity: AlertSeverity.Warning, triggeredMS: 1, lastCheckedMS: 0, }, @@ -160,9 +168,15 @@ describe('ClusterHealthAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if the cluster health is green', async () => { + (fetchClusterHealth as jest.Mock).mockImplementation(() => { + return [ + { + health: AlertClusterHealthType.Green, + clusterUuid, + ccs, + }, + ]; }); const alert = new ClusterHealthAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts index 63f658d5b0283..c4e5de3d55356 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -13,13 +13,23 @@ import { AlertState, AlertMessage, AlertMessageLinkToken, - LegacyAlert, + CommonAlertParams, + AlertClusterHealth, + AlertInstanceState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_CLUSTER_HEALTH, LEGACY_ALERT_DETAILS } from '../../common/constants'; -import { AlertMessageTokenType, AlertClusterHealthType } from '../../common/enums'; +import { + ALERT_CLUSTER_HEALTH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; +import { AlertMessageTokenType, AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterHealth.redMessage', { defaultMessage: 'Allocate missing primary and replica shards', @@ -37,12 +47,6 @@ export class ClusterHealthAlert extends BaseAlert { super(rawAlert, { id: ALERT_CLUSTER_HEALTH, name: LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label, - legacy: { - watchName: 'elasticsearch_cluster_status', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.clusterHealth.nodeNameLabel', { - defaultMessage: 'Elasticsearch cluster alert', - }), - }, actionVariables: [ { name: 'clusterHealth', @@ -58,15 +62,36 @@ export class ClusterHealthAlert extends BaseAlert { }); } - private getHealth(legacyAlert: LegacyAlert) { - return legacyAlert.prefix - .replace('Elasticsearch cluster status is ', '') - .slice(0, -1) as AlertClusterHealthType; + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const healths = await fetchClusterHealth(callCluster, clusters, esIndexPattern); + return healths.map((clusterHealth) => { + const shouldFire = clusterHealth.health !== AlertClusterHealthType.Green; + const severity = + clusterHealth.health === AlertClusterHealthType.Red + ? AlertSeverity.Danger + : AlertSeverity.Warning; + + return { + shouldFire, + severity, + meta: clusterHealth, + clusterUuid: clusterHealth.clusterUuid, + ccs: clusterHealth.ccs, + }; + }); } protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const health = this.getHealth(legacyAlert); + const { health } = item.meta as AlertClusterHealth; return { text: i18n.translate('xpack.monitoring.alerts.clusterHealth.ui.firingMessage', { defaultMessage: `Elasticsearch cluster health is {health}.`, @@ -98,52 +123,56 @@ export class ClusterHealthAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const health = this.getHealth(legacyAlert); - if (alertState.ui.isFiring) { - const actionText = - health === AlertClusterHealthType.Red - ? i18n.translate('xpack.monitoring.alerts.clusterHealth.action.danger', { - defaultMessage: `Allocate missing primary and replica shards.`, - }) - : i18n.translate('xpack.monitoring.alerts.clusterHealth.action.warning', { - defaultMessage: `Allocate missing replica shards.`, - }); - - const action = `[${actionText}](elasticsearch/indices)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', - { - defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, - values: { - clusterName: cluster.clusterName, - health, - actionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', - { - defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, - values: { - clusterName: cluster.clusterName, - health, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterHealth: health, - clusterName: cluster.clusterName, - action, - actionPlain: actionText, - }); + if (alertStates.length === 0) { + return; } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { health } = state.meta as AlertClusterHealth; + const actionText = + health === AlertClusterHealthType.Red + ? i18n.translate('xpack.monitoring.alerts.clusterHealth.action.danger', { + defaultMessage: `Allocate missing primary and replica shards.`, + }) + : i18n.translate('xpack.monitoring.alerts.clusterHealth.action.warning', { + defaultMessage: `Allocate missing replica shards.`, + }); + + const action = `[${actionText}](elasticsearch/indices)`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, + values: { + clusterName: cluster.clusterName, + health, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, + values: { + clusterName: cluster.clusterName, + health, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterHealth: health, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index 5f9ea3a18b570..a231cec762191 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -7,13 +7,13 @@ import { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; import { ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_elasticsearch_versions', () => ({ + fetchElasticsearchVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -67,16 +68,16 @@ describe('ElasticsearchVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Elasticsearch.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const elasticsearchVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -98,8 +99,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchElasticsearchVersions as jest.Mock).mockImplementation(() => { + return elasticsearchVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -125,13 +126,19 @@ describe('ElasticsearchVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Elasticsearch node alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: - 'Multiple versions of Elasticsearch ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Elasticsearch (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -141,21 +148,26 @@ describe('ElasticsearchVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View nodes](elasticsearch/nodes)', + action: `[View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all nodes.', - internalFullMessage: - 'Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running [8.0.0, 7.2.1]. [View nodes](elasticsearch/nodes)', + internalFullMessage: `Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if there is no mismatch', async () => { + (fetchElasticsearchVersions as jest.Mock).mockImplementation(() => { + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new ElasticsearchVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts index 717d803084c6f..e8e93e4b3afec 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -12,29 +12,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_ELASTICSEARCH_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_ELASTICSEARCH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; export class ElasticsearchVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_ELASTICSEARCH_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label, - legacy: { - watchName: 'elasticsearch_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Elasticsearch node alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -51,15 +51,42 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const elasticsearchVersions = await fetchElasticsearchVersions( + callCluster, + clusters, + esIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return elasticsearchVersions.map((elasticsearchVersion) => { + return { + shouldFire: elasticsearchVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: elasticsearchVersion, + clusterUuid: elasticsearchVersion.clusterUuid, + ccs: elasticsearchVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate( 'xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Elasticsearch ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, } ); @@ -71,54 +98,63 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction', + if (alertStates.length === 0) { + return; + } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'elasticsearch/nodes', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'Verify you have the same version across all nodes.', + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction', + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', { - defaultMessage: 'View nodes', + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, } - ); - const action = `[${fullActionText}](elasticsearch/nodes)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index a6cc7445cb764..6252fc59ba246 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -7,13 +7,13 @@ import { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; import { ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_kibana_versions', () => ({ + fetchKibanaVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -70,16 +71,16 @@ describe('KibanaVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Kibana.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const kibanaVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -101,8 +102,8 @@ describe('KibanaVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchKibanaVersions as jest.Mock).mockImplementation(() => { + return kibanaVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -127,12 +128,19 @@ describe('KibanaVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Kibana instance alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: 'Multiple versions of Kibana ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Kibana (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -142,21 +150,26 @@ describe('KibanaVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View instances](kibana/instances)', + action: `[View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all instances.', - internalFullMessage: - 'Kibana version mismatch alert is firing for testCluster. Kibana is running [8.0.0, 7.2.1]. [View instances](kibana/instances)', + internalFullMessage: `Kibana version mismatch alert is firing for testCluster. Kibana is running 8.0.0, 7.2.1. [View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if there is no mismatch', async () => { + (fetchKibanaVersions as jest.Mock).mockImplementation(() => { + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new KibanaVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts index 4fe71e7c27146..f1f8959787003 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -12,29 +12,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_KIBANA_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_KIBANA_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_KIBANA, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; export class KibanaVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_KIBANA_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label, - legacy: { - watchName: 'kibana_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Kibana instance alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -64,13 +64,40 @@ export class KibanaVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let kibanaIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_KIBANA); + if (availableCcs) { + kibanaIndexPattern = getCcsIndexPattern(kibanaIndexPattern, availableCcs); + } + const kibanaVersions = await fetchKibanaVersions( + callCluster, + clusters, + kibanaIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return kibanaVersions.map((kibanaVersion) => { + return { + shouldFire: kibanaVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: kibanaVersion, + clusterUuid: kibanaVersion.clusterUuid, + ccs: kibanaVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Kibana ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, }); @@ -81,54 +108,64 @@ export class KibanaVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.shortAction', - { - defaultMessage: 'Verify you have the same version across all instances.', - } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.fullAction', + if (alertStates.length === 0) { + return; + } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all instances.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.fullAction', + { + defaultMessage: 'View instances', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'kibana/instances', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. Kibana is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, + } + ); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'View instances', + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const action = `[${fullActionText}](kibana/instances)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. Kibana is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index fa2740eb9aa1e..0d1c1d20097e5 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -7,23 +7,20 @@ import { LicenseExpirationAlert } from './license_expiration_alert'; import { ALERT_LICENSE_EXPIRATION } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { AlertSeverity } from '../../common/enums'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_licenses', () => ({ + fetchLicenses: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), })); jest.mock('moment', () => { - const moment = function () { - return { - format: () => 'THE_DATE', - }; - }; + const moment = function () {}; moment.duration = () => ({ humanize: () => 'HUMANIZED_DURATION' }); return moment; }); @@ -76,15 +73,11 @@ describe('LicenseExpirationAlert', () => { const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: - 'The license for this cluster expires in {{#relativeTime}}metadata.time{{/relativeTime}} at {{#absoluteTime}}metadata.time{{/absoluteTime}}.', - message: 'Update your license.', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, - time: 1, - }, + const license = { + status: 'expired', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 59, + clusterUuid, }; const replaceState = jest.fn(); @@ -107,8 +100,8 @@ describe('LicenseExpirationAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [license]; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -134,7 +127,15 @@ describe('LicenseExpirationAlert', () => { { cluster: { clusterUuid, clusterName }, ccs: undefined, - nodeName: 'Elasticsearch cluster alert', + itemLabel: undefined, + meta: { + clusterUuid: 'abc123', + expiryDateMS: 5097600000, + status: 'expired', + type: 'gold', + }, + nodeId: undefined, + nodeName: undefined, ui: { isFiring: true, message: { @@ -146,14 +147,14 @@ describe('LicenseExpirationAlert', () => { type: 'time', isRelative: true, isAbsolute: false, - timestamp: 1, + timestamp: 5097600000, }, { startToken: '#absolute', type: 'time', isAbsolute: true, isRelative: false, - timestamp: 1, + timestamp: 5097600000, }, { startToken: '#start_link', @@ -163,7 +164,7 @@ describe('LicenseExpirationAlert', () => { }, ], }, - severity: 'warning', + severity: 'danger', triggeredMS: 1, lastCheckedMS: 0, }, @@ -183,9 +184,16 @@ describe('LicenseExpirationAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if the license is not expired', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 61, + clusterUuid, + }, + ]; }); const alert = new LicenseExpirationAlert(); const type = alert.getAlertType(); @@ -197,5 +205,47 @@ describe('LicenseExpirationAlert', () => { expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); }); + + it('should use danger severity for a license expiring soon', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 2, + clusterUuid, + }, + ]; + }); + const alert = new LicenseExpirationAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Danger); + }); + + it('should use warning severity for a license expiring in a bit', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 31, + clusterUuid, + }, + ]; + }); + const alert = new LicenseExpirationAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Warning); + }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index cd59fa63f3b2e..24fbd98ef2e8b 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { BaseAlert } from './base_alert'; @@ -15,26 +14,32 @@ import { AlertMessage, AlertMessageTimeToken, AlertMessageLinkToken, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertLicense, + AlertLicenseState, } from '../../common/types/alerts'; import { AlertExecutorOptions, AlertInstance } from '../../../alerts/server'; -import { ALERT_LICENSE_EXPIRATION, LEGACY_ALERT_DETAILS } from '../../common/constants'; -import { AlertMessageTokenType } from '../../common/enums'; +import { + ALERT_LICENSE_EXPIRATION, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; + +const EXPIRES_DAYS = [60, 30, 14, 7]; export class LicenseExpirationAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_LICENSE_EXPIRATION, name: LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label, - legacy: { - watchName: 'xpack_license_expiration', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.licenseExpiration.nodeNameLabel', { - defaultMessage: 'Elasticsearch cluster alert', - }), - }, interval: '1d', actionVariables: [ { @@ -71,8 +76,53 @@ export class LicenseExpirationAlert extends BaseAlert { return await super.execute(options); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const licenses = await fetchLicenses(callCluster, clusters, esIndexPattern); + + return licenses.map((license) => { + const { clusterUuid, type, expiryDateMS, status, ccs } = license; + let isExpired = false; + let severity = AlertSeverity.Success; + + if (status !== 'active') { + isExpired = true; + severity = AlertSeverity.Danger; + } else if (expiryDateMS) { + for (let i = EXPIRES_DAYS.length - 1; i >= 0; i--) { + if (type === 'trial' && i < 2) { + break; + } + + const fromNow = +new Date() + EXPIRES_DAYS[i] * 1000 * 60 * 60 * 24; + if (fromNow >= expiryDateMS) { + isExpired = true; + severity = i < 1 ? AlertSeverity.Warning : AlertSeverity.Danger; + break; + } + } + } + + return { + shouldFire: isExpired, + severity, + meta: license, + clusterUuid, + ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; + const license = item.meta as AlertLicense; return { text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { defaultMessage: `The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link`, @@ -83,14 +133,14 @@ export class LicenseExpirationAlert extends BaseAlert { type: AlertMessageTokenType.Time, isRelative: true, isAbsolute: false, - timestamp: legacyAlert.metadata.time, + timestamp: license.expiryDateMS, } as AlertMessageTimeToken, { startToken: '#absolute', type: AlertMessageTokenType.Time, isAbsolute: true, isRelative: false, - timestamp: legacyAlert.metadata.time, + timestamp: license.expiryDateMS, } as AlertMessageTimeToken, { startToken: '#start_link', @@ -104,48 +154,51 @@ export class LicenseExpirationAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const $expiry = moment(legacyAlert.metadata.time); - const $duration = moment.duration(+new Date() - $expiry.valueOf()); - if (alertState.ui.isFiring) { - const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { - defaultMessage: 'Please update your license.', - }); - const action = `[${actionText}](elasticsearch/nodes)`; - const expiredDate = $duration.humanize(); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', - { - defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, - values: { - clusterName: cluster.clusterName, - expiredDate, - actionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', - { - defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, - values: { - clusterName: cluster.clusterName, - expiredDate, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - expiredDate, - clusterName: cluster.clusterName, - action, - actionPlain: actionText, - }); + if (alertStates.length === 0) { + return; } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state: AlertLicenseState = alertStates[0] as AlertLicenseState; + const $duration = moment.duration(+new Date() - state.expiryDateMS); + const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { + defaultMessage: 'Please update your license.', + }); + const action = `[${actionText}](elasticsearch/nodes)`; + const expiredDate = $duration.humanize(); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + expiredDate, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index 514fd71368085..50a826b36d58f 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -7,13 +7,13 @@ import { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; import { ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_logstash_versions', () => ({ + fetchLogstashVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -68,16 +69,16 @@ describe('LogstashVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Logstash.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const logstashVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -99,8 +100,8 @@ describe('LogstashVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchLogstashVersions as jest.Mock).mockImplementation(() => { + return logstashVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -126,12 +127,19 @@ describe('LogstashVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Logstash node alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: 'Multiple versions of Logstash ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Logstash (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -141,21 +149,26 @@ describe('LogstashVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View nodes](logstash/nodes)', + action: `[View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all nodes.', - internalFullMessage: - 'Logstash version mismatch alert is firing for testCluster. Logstash is running [8.0.0, 7.2.1]. [View nodes](logstash/nodes)', + internalFullMessage: `Logstash version mismatch alert is firing for testCluster. Logstash is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if there is no mismatch', async () => { + (fetchLogstashVersions as jest.Mock).mockImplementation(() => { + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new LogstashVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts index 0dc93743e2276..d903dd49600ad 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -12,29 +12,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_LOGSTASH_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_LOGSTASH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_LOGSTASH, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; export class LogstashVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_LOGSTASH_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label, - legacy: { - watchName: 'logstash_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Logstash node alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -51,15 +51,42 @@ export class LogstashVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let logstashIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_LOGSTASH); + if (availableCcs) { + logstashIndexPattern = getCcsIndexPattern(logstashIndexPattern, availableCcs); + } + const logstashVersions = await fetchLogstashVersions( + callCluster, + clusters, + logstashIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return logstashVersions.map((logstashVersion) => { + return { + shouldFire: logstashVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: logstashVersion, + clusterUuid: logstashVersion.clusterUuid, + ccs: logstashVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate( 'xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Logstash ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, } ); @@ -71,54 +98,63 @@ export class LogstashVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.shortAction', + if (alertStates.length === 0) { + return; + } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'logstash/nodes', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'Verify you have the same version across all nodes.', + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.fullAction', + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', { - defaultMessage: 'View nodes', + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, } - ); - const action = `[${fullActionText}](logstash/nodes)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index 59b61645e2eca..848436573fab9 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -7,13 +7,13 @@ import { NodesChangedAlert } from './nodes_changed_alert'; import { ALERT_NODES_CHANGED } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_nodes_from_cluster_stats', () => ({ + fetchNodesFromClusterStats: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -73,23 +73,33 @@ describe('NodesChangedAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const nodeUuid = 'myNodeUuid'; + const nodeEphemeralId = 'myEphemeralId'; + const nodeEphemeralIdChanged = 'myEphemeralIdChanged'; + const nodeName = 'test'; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'Elasticsearch cluster nodes have changed!', - message: 'Node was restarted [1]: [test].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, - }, - nodes: { - added: {}, - removed: {}, - restarted: { - test: 'test', - }, + const nodes = [ + { + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -111,8 +121,8 @@ describe('NodesChangedAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { + return nodes; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -138,8 +148,28 @@ describe('NodesChangedAlert', () => { alertStates: [ { cluster: { clusterUuid, clusterName }, - ccs: undefined, - nodeName: 'Elasticsearch nodes alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + }, ui: { isFiring: true, message: { @@ -167,9 +197,28 @@ describe('NodesChangedAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if no nodes have changed', async () => { + (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { + return [ + { + recentNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + clusterUuid, + ccs, + }, + ]; }); const alert = new NodesChangedAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index 10dc6f911409e..63b3ef672405e 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -12,26 +12,61 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, - LegacyAlertNodesChangedList, + AlertClusterStatsNodes, + AlertClusterStatsNode, + CommonAlertParams, + AlertInstanceState, + AlertNodesChangedState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_NODES_CHANGED, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_NODES_CHANGED, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { AlertSeverity } from '../../common/enums'; + +interface AlertNodesChangedStates { + removed: AlertClusterStatsNode[]; + added: AlertClusterStatsNode[]; + restarted: AlertClusterStatsNode[]; +} + +function getNodeStates(nodes: AlertClusterStatsNodes): AlertNodesChangedStates { + const removed = nodes.priorNodes.filter( + (priorNode) => + !nodes.recentNodes.find((recentNode) => priorNode.nodeUuid === recentNode.nodeUuid) + ); + const added = nodes.recentNodes.filter( + (recentNode) => + !nodes.priorNodes.find((priorNode) => priorNode.nodeUuid === recentNode.nodeUuid) + ); + const restarted = nodes.recentNodes.filter( + (recentNode) => + nodes.priorNodes.find((priorNode) => priorNode.nodeUuid === recentNode.nodeUuid) && + !nodes.priorNodes.find( + (priorNode) => priorNode.nodeEphemeralId === recentNode.nodeEphemeralId + ) + ); + + return { + removed, + added, + restarted, + }; +} export class NodesChangedAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_NODES_CHANGED, name: LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label, - legacy: { - watchName: 'elasticsearch_nodes', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.nodesChanged.nodeNameLabel', { - defaultMessage: 'Elasticsearch nodes alert', - }), - changeDataValues: { shouldFire: true }, - }, actionVariables: [ { name: 'added', @@ -65,13 +100,39 @@ export class NodesChangedAlert extends BaseAlert { }); } - private getNodeStates(legacyAlert: LegacyAlert): LegacyAlertNodesChangedList { - return legacyAlert.nodes || { added: {}, removed: {}, restarted: {} }; + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const nodesFromClusterStats = await fetchNodesFromClusterStats( + callCluster, + clusters, + esIndexPattern + ); + return nodesFromClusterStats.map((nodes) => { + const { removed, added, restarted } = getNodeStates(nodes); + const shouldFire = removed.length > 0 || added.length > 0 || restarted.length > 0; + const severity = AlertSeverity.Warning; + + return { + shouldFire, + severity, + meta: nodes, + clusterUuid: nodes.clusterUuid, + ccs: nodes.ccs, + }; + }); } protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const states = this.getNodeStates(legacyAlert); + const nodes = item.meta as AlertClusterStatsNodes; + const states = getNodeStates(nodes); if (!alertState.ui.isFiring) { return { text: i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage', { @@ -80,11 +141,7 @@ export class NodesChangedAlert extends BaseAlert { }; } - if ( - Object.values(states.added).length === 0 && - Object.values(states.removed).length === 0 && - Object.values(states.restarted).length === 0 - ) { + if (states.added.length === 0 && states.removed.length === 0 && states.restarted.length === 0) { return { text: i18n.translate( 'xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage', @@ -96,29 +153,29 @@ export class NodesChangedAlert extends BaseAlert { } const addedText = - Object.values(states.added).length > 0 + states.added.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage', { defaultMessage: `Elasticsearch nodes '{added}' added to this cluster.`, values: { - added: Object.values(states.added).join(','), + added: states.added.map((n) => n.nodeName).join(','), }, }) : null; const removedText = - Object.values(states.removed).length > 0 + states.removed.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage', { defaultMessage: `Elasticsearch nodes '{removed}' removed from this cluster.`, values: { - removed: Object.values(states.removed).join(','), + removed: states.removed.map((n) => n.nodeName).join(','), }, }) : null; const restartedText = - Object.values(states.restarted).length > 0 + states.restarted.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage', { defaultMessage: `Elasticsearch nodes '{restarted}' restarted in this cluster.`, values: { - restarted: Object.values(states.restarted).join(','), + restarted: states.restarted.map((n) => n.nodeName).join(','), }, }) : null; @@ -130,55 +187,60 @@ export class NodesChangedAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { - defaultMessage: 'Verify that you added, removed, or restarted nodes.', - }); - const fullActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.fullAction', { - defaultMessage: 'View nodes', - }); - const action = `[${fullActionText}](elasticsearch/nodes)`; - const states = this.getNodeStates(legacyAlert); - const added = Object.values(states.added).join(','); - const removed = Object.values(states.removed).join(','); - const restarted = Object.values(states.restarted).join(','); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', - { - defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', - { - defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, - values: { - clusterName: cluster.clusterName, - added, - removed, - restarted, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - added, - removed, - restarted, - action, - actionPlain: shortActionText, - }); + if (alertStates.length === 0) { + return; } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0] as AlertNodesChangedState; + const nodes = state.meta as AlertClusterStatsNodes; + const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { + defaultMessage: 'Verify that you added, removed, or restarted nodes.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.fullAction', { + defaultMessage: 'View nodes', + }); + const action = `[${fullActionText}](elasticsearch/nodes)`; + const states = getNodeStates(nodes); + const added = states.added.map((node) => node.nodeName).join(','); + const removed = states.removed.map((node) => node.nodeName).join(','); + const restarted = states.restarted.map((node) => node.nodeName).join(','); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, + values: { + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts new file mode 100644 index 0000000000000..2fdbbe80b7e89 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fetchClusterHealth } from './fetch_cluster_health'; + +describe('fetchClusterHealth', () => { + it('should return the cluster health', async () => { + const status = 'green'; + const clusterUuid = 'sdfdsaj34434'; + const callCluster = jest.fn(() => ({ + hits: { + hits: [ + { + _index: '.monitoring-es-7', + _source: { + cluster_state: { + status, + }, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + + const clusters = [{ clusterUuid, clusterName: 'foo' }]; + const index = '.monitoring-es-*'; + + const health = await fetchClusterHealth(callCluster, clusters, index); + expect(health).toEqual([ + { + health: status, + clusterUuid, + ccs: undefined, + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts new file mode 100644 index 0000000000000..bcfa2da0958a2 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertCluster, AlertClusterHealth } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +export async function fetchClusterHealth( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.cluster_state.status', + 'hits.hits._source.cluster_uuid', + 'hits.hits._index', + ], + body: { + size: clusters.length, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { + return { + health: hit._source.cluster_state?.status, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index.includes(':') ? hit._index.split(':')[0] : undefined, + } as AlertClusterHealth; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts new file mode 100644 index 0000000000000..e4f4a4d364ebf --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fetchElasticsearchVersions } from './fetch_elasticsearch_versions'; + +describe('fetchElasticsearchVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-es-*'; + const size = 10; + const versions = ['8.0.0', '7.2.1']; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + hits: { + hits: [ + { + _index: `Monitoring:${index}`, + _source: { + cluster_uuid: 'cluster123', + cluster_stats: { + nodes: { + versions, + }, + }, + }, + }, + ], + }, + }; + }); + + const result = await fetchElasticsearchVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions, + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts new file mode 100644 index 0000000000000..373ddb62aaee8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +export async function fetchElasticsearchVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.cluster_stats.nodes.versions', + 'hits.hits._index', + 'hits.hits._source.cluster_uuid', + ], + body: { + size: clusters.length, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { + const versions = hit._source.cluster_stats?.nodes?.versions; + return { + versions, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index.includes(':') ? hit._index.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts new file mode 100644 index 0000000000000..518828ef0b1c8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fetchKibanaVersions } from './fetch_kibana_versions'; + +describe('fetchKibanaVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-kibana-*'; + const size = 10; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + aggregations: { + index: { + buckets: [ + { + key: `Monitoring:${index}`, + }, + ], + }, + cluster: { + buckets: [ + { + key: 'cluster123', + group_by_kibana: { + buckets: [ + { + group_by_version: { + buckets: [ + { + key: '8.0.0', + }, + ], + }, + }, + { + group_by_version: { + buckets: [ + { + key: '7.2.1', + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + + const result = await fetchKibanaVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions: ['8.0.0', '7.2.1'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts new file mode 100644 index 0000000000000..2e7fe192df656 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { get } from 'lodash'; +import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; + +interface ESAggResponse { + key: string; +} + +export async function fetchKibanaVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'kibana_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 1, + }, + }, + cluster: { + terms: { + field: 'cluster_uuid', + size: 1, + }, + aggs: { + group_by_kibana: { + terms: { + field: 'kibana_stats.kibana.uuid', + size, + }, + aggs: { + group_by_version: { + terms: { + field: 'kibana_stats.kibana.version', + size: 1, + order: { + latest_report: 'desc', + }, + }, + aggs: { + latest_report: { + max: { + field: 'timestamp', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const indexName = get(response, 'aggregations.index.buckets[0].key', ''); + const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; + return clusterList.map((cluster) => { + const clusterUuid = cluster.key; + const uuids = get(cluster, 'group_by_kibana.buckets', []); + const byVersion: { [version: string]: boolean } = {}; + for (const uuid of uuids) { + const version = get(uuid, 'group_by_version.buckets[0].key', ''); + if (!version) { + continue; + } + byVersion[version] = true; + } + return { + versions: Object.keys(byVersion), + clusterUuid, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts deleted file mode 100644 index 086c5c7da9139..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts +++ /dev/null @@ -1,96 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { fetchLegacyAlerts } from './fetch_legacy_alerts'; - -describe('fetchLegacyAlerts', () => { - let callCluster = jest.fn(); - const clusters = [ - { - clusterUuid: 'abc123', - clusterName: 'test', - }, - ]; - const index = '.monitoring-es-*'; - const size = 10; - - it('fetch legacy alerts', async () => { - const prefix = 'thePrefix'; - const message = 'theMessage'; - const nodes = {}; - const metadata = { - severity: 2000, - cluster_uuid: clusters[0].clusterUuid, - metadata: {}, - }; - callCluster = jest.fn().mockImplementation(() => { - return { - hits: { - hits: [ - { - _source: { - prefix, - message, - nodes, - metadata, - }, - }, - ], - }, - }; - }); - const result = await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); - expect(result).toEqual([ - { - message, - metadata, - nodes, - nodeName: '', - prefix, - }, - ]); - }); - - it('should use consistent params', async () => { - let params = null; - callCluster = jest.fn().mockImplementation((...args) => { - params = args[1]; - }); - await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); - expect(params).toStrictEqual({ - index, - filterPath: [ - 'hits.hits._source.prefix', - 'hits.hits._source.message', - 'hits.hits._source.resolved_timestamp', - 'hits.hits._source.nodes', - 'hits.hits._source.metadata.*', - ], - body: { - size, - sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], - query: { - bool: { - minimum_should_match: 1, - filter: [ - { - terms: { 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid) }, - }, - { term: { 'metadata.watch': 'myWatch' } }, - ], - should: [ - { range: { timestamp: { gte: 'now-2m' } } }, - { range: { resolved_timestamp: { gte: 'now-2m' } } }, - { bool: { must_not: { exists: { field: 'resolved_timestamp' } } } }, - ], - }, - }, - collapse: { field: 'metadata.cluster_uuid' }, - }, - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts deleted file mode 100644 index 96438da111b6d..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts +++ /dev/null @@ -1,97 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get } from 'lodash'; -import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../../common/types/alerts'; - -export async function fetchLegacyAlerts( - callCluster: any, - clusters: AlertCluster[], - index: string, - watchName: string, - size: number -): Promise { - const params = { - index, - filterPath: [ - 'hits.hits._source.prefix', - 'hits.hits._source.message', - 'hits.hits._source.resolved_timestamp', - 'hits.hits._source.nodes', - 'hits.hits._source.metadata.*', - ], - body: { - size, - sort: [ - { - timestamp: { - order: 'desc', - unmapped_type: 'long', - }, - }, - ], - query: { - bool: { - minimum_should_match: 1, - filter: [ - { - terms: { - 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid), - }, - }, - { - term: { - 'metadata.watch': watchName, - }, - }, - ], - should: [ - { - range: { - timestamp: { - gte: 'now-2m', - }, - }, - }, - { - range: { - resolved_timestamp: { - gte: 'now-2m', - }, - }, - }, - { - bool: { - must_not: { - exists: { - field: 'resolved_timestamp', - }, - }, - }, - }, - ], - }, - }, - collapse: { - field: 'metadata.cluster_uuid', - }, - }, - }; - - const response = await callCluster('search', params); - return get(response, 'hits.hits', []).map((hit: any) => { - const legacyAlert: LegacyAlert = { - prefix: get(hit, '_source.prefix'), - message: get(hit, '_source.message'), - resolved_timestamp: get(hit, '_source.resolved_timestamp'), - nodes: get(hit, '_source.nodes'), - nodeName: '', // This is set by BaseAlert - metadata: get(hit, '_source.metadata') as LegacyAlertMetadata, - }; - return legacyAlert; - }); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts new file mode 100644 index 0000000000000..715c8c50a45e7 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { fetchLicenses } from './fetch_licenses'; + +describe('fetchLicenses', () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + + it('return a list of licenses', async () => { + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + }, + ]); + }); + + it('should only search for the clusters provided', async () => { + const callCluster = jest.fn(); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); + }); + + it('should limit the time period in the query', async () => { + const callCluster = jest.fn(); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts new file mode 100644 index 0000000000000..6cec7f3296926 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertLicense, AlertCluster } from '../../../common/types/alerts'; +import { ElasticsearchResponse } from '../../../common/types/es'; + +export async function fetchLicenses( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.license.*', + 'hits.hits._source.cluster_uuid', + 'hits.hits._index', + ], + body: { + size: clusters.length, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response: ElasticsearchResponse = await callCluster('search', params); + return ( + response?.hits?.hits.map((hit) => { + const rawLicense = hit._source.license ?? {}; + const license: AlertLicense = { + status: rawLicense.status ?? '', + type: rawLicense.type ?? '', + expiryDateMS: rawLicense.expiry_date_in_millis ?? 0, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index, + }; + return license; + }) ?? [] + ); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts new file mode 100644 index 0000000000000..a739593df27e9 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fetchLogstashVersions } from './fetch_logstash_versions'; + +describe('fetchLogstashVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-logstash-*'; + const size = 10; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + aggregations: { + index: { + buckets: [ + { + key: `Monitoring:${index}`, + }, + ], + }, + cluster: { + buckets: [ + { + key: 'cluster123', + group_by_logstash: { + buckets: [ + { + group_by_version: { + buckets: [ + { + key: '8.0.0', + }, + ], + }, + }, + { + group_by_version: { + buckets: [ + { + key: '7.2.1', + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + + const result = await fetchLogstashVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions: ['8.0.0', '7.2.1'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts new file mode 100644 index 0000000000000..8f20c64d6243e --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { get } from 'lodash'; +import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; + +interface ESAggResponse { + key: string; +} + +export async function fetchLogstashVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'logstash_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 1, + }, + }, + cluster: { + terms: { + field: 'cluster_uuid', + size: 1, + }, + aggs: { + group_by_logstash: { + terms: { + field: 'logstash_stats.logstash.uuid', + size, + }, + aggs: { + group_by_version: { + terms: { + field: 'logstash_stats.logstash.version', + size: 1, + order: { + latest_report: 'desc', + }, + }, + aggs: { + latest_report: { + max: { + field: 'timestamp', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const indexName = get(response, 'aggregations.index.buckets[0].key', ''); + const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; + return clusterList.map((cluster) => { + const clusterUuid = cluster.key; + const uuids = get(cluster, 'group_by_logstash.buckets', []); + const byVersion: { [version: string]: boolean } = {}; + for (const uuid of uuids) { + const version = get(uuid, 'group_by_version.buckets[0].key', ''); + if (!version) { + continue; + } + byVersion[version] = true; + } + return { + versions: Object.keys(byVersion), + clusterUuid, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts new file mode 100644 index 0000000000000..c399594c170fa --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertCluster, AlertClusterStatsNodes } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +function formatNode( + nodes: NonNullable['nodes']> | undefined +) { + if (!nodes) { + return []; + } + return Object.keys(nodes).map((nodeUuid) => { + return { + nodeUuid, + nodeEphemeralId: nodes[nodeUuid].ephemeral_id, + nodeName: nodes[nodeUuid].name, + }; + }); +} + +export async function fetchNodesFromClusterStats( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: ['aggregations.clusters.buckets'], + body: { + size: 0, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + include: clusters.map((cluster) => cluster.clusterUuid), + field: 'cluster_uuid', + }, + aggs: { + top: { + top_hits: { + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + _source: { + includes: ['cluster_state.nodes_hash', 'cluster_state.nodes'], + }, + size: 2, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const nodes = []; + const clusterBuckets = response.aggregations.clusters.buckets; + for (const clusterBucket of clusterBuckets) { + const clusterUuid = clusterBucket.key; + const hits = clusterBucket.top.hits.hits; + const indexName = hits[0]._index; + nodes.push({ + clusterUuid, + recentNodes: formatNode(hits[0]._source.cluster_state?.nodes), + priorNodes: formatNode(hits[1]._source.cluster_state?.nodes), + ccs: indexName.includes(':') ? indexName.split(':')[0] : undefined, + }); + } + return nodes; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 6c08a0b3db758..399b26a6c5c31 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -28,7 +28,7 @@ export async function fetchStatus( await Promise.all( (alertTypes || ALERTS).map(async (type) => { const alert = await AlertsFactory.getByType(type, alertsClient); - if (!alert || !alert.isEnabled(licenseService) || !alert.rawAlert) { + if (!alert || !alert.rawAlert) { return; } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index a8389e26d4f9f..901ea96d525e8 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -25,8 +25,7 @@ export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) }, async (context, request, response) => { try { - const alerts = AlertsFactory.getAll().filter((a) => a.isEnabled(npRoute.licenseService)); - + const alerts = AlertsFactory.getAll(); if (alerts.length) { const { isSufficientlySecure, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6658671b84682..168eb14966493 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14427,7 +14427,6 @@ "xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。現在の正常性は{health}です。{action}", "xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。現在の正常性は{health}です。{actionText}", "xpack.monitoring.alerts.clusterHealth.label": "クラスターの正常性", - "xpack.monitoring.alerts.clusterHealth.nodeNameLabel": "Elasticsearch クラスターアラート", "xpack.monitoring.alerts.clusterHealth.redMessage": "見つからないプライマリおよびレプリカシャードを割り当て", "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearchクラスターの正常性は{health}です。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}. #start_linkView now#end_link", @@ -14467,7 +14466,6 @@ "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "{clusterName}に対してElasticsearchバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction": "ノードの表示", "xpack.monitoring.alerts.elasticsearchVersionMismatch.label": "Elasticsearch バージョン不一致", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel": "Elasticsearch ノードアラート", "xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Elasticsearch({versions})が実行されています。", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel": "{timeValue, plural, other {日}}", @@ -14481,7 +14479,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage": "{clusterName}に対してKibanaバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.kibanaVersionMismatch.fullAction": "インスタンスを表示", "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana バージョン不一致", - "xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel": "Kibana インスタンスアラート", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "すべてのインスタンスのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Kibana({versions})が実行されています。", "xpack.monitoring.alerts.legacyAlert.expressionText": "構成するものがありません。", @@ -14492,7 +14489,6 @@ "xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage": "ライセンス有効期限アラートが {clusterName} に対して実行されています。ライセンスは{expiredDate}に期限切れになります。{action}", "xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage": "ライセンス有効期限アラートが {clusterName} に対して実行されています。ライセンスは{expiredDate}に期限切れになります。{actionText}", "xpack.monitoring.alerts.licenseExpiration.label": "ライセンス期限", - "xpack.monitoring.alerts.licenseExpiration.nodeNameLabel": "Elasticsearch クラスターアラート", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "このクラスターのライセンスは#absoluteの#relativeに期限切れになります。#start_linkライセンスを更新してください。#end_link", "xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth": "このクラスターを実行している Logstash のバージョン。", "xpack.monitoring.alerts.logstashVersionMismatch.description": "クラスターに複数のバージョンの Logstash があるときにアラートを発行します。", @@ -14500,7 +14496,6 @@ "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage": "{clusterName}に対してLogstashバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.logstashVersionMismatch.fullAction": "ノードの表示", "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash バージョン不一致", - "xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel": "Logstash ノードアラート", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Logstash({versions})が実行されています。", "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "高メモリー使用率を報告しているノード数。", @@ -14543,7 +14538,6 @@ "xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage": "{clusterName}に対してノード変更アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.nodesChanged.fullAction": "ノードの表示", "xpack.monitoring.alerts.nodesChanged.label": "ノードが変更されました", - "xpack.monitoring.alerts.nodesChanged.nodeNameLabel": "Elasticsearch ノードアラート", "xpack.monitoring.alerts.nodesChanged.shortAction": "ノードを追加、削除、または再起動したことを確認してください。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearchノード「{added}」がこのクラスターに追加されました。", "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearchノードが変更されました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9602583e8d215..129deb575a52f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14469,7 +14469,6 @@ "xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage": "为 {clusterName} 触发了集群运行状况告警。当前运行状况为 {health}。{action}", "xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage": "为 {clusterName} 触发了集群运行状况告警。当前运行状况为 {health}。{actionText}", "xpack.monitoring.alerts.clusterHealth.label": "集群运行状况", - "xpack.monitoring.alerts.clusterHealth.nodeNameLabel": "Elasticsearch 集群告警", "xpack.monitoring.alerts.clusterHealth.redMessage": "分配缺失的主分片和副本分片", "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearch 集群运行状况为 {health}。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}。#start_link立即查看#end_link", @@ -14509,7 +14508,6 @@ "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Elasticsearch 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction": "查看节点", "xpack.monitoring.alerts.elasticsearchVersionMismatch.label": "Elasticsearch 版本不匹配", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel": "Elasticsearch 节点告警", "xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Elasticsearch ({versions}) 版本。", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel": "{timeValue, plural, other {天}}", @@ -14523,7 +14521,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Kibana 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.kibanaVersionMismatch.fullAction": "查看实例", "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana 版本不匹配", - "xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel": "Kibana 实例告警", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "确认所有实例具有相同的版本。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Kibana 版本 ({versions})。", "xpack.monitoring.alerts.legacyAlert.expressionText": "没有可配置的内容。", @@ -14534,7 +14531,6 @@ "xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage": "为 {clusterName} 触发了许可证到期告警。您的许可证将于 {expiredDate}到期。{action}", "xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage": "为 {clusterName} 触发了许可证到期告警。您的许可证将于 {expiredDate}到期。{actionText}", "xpack.monitoring.alerts.licenseExpiration.label": "许可证到期", - "xpack.monitoring.alerts.licenseExpiration.nodeNameLabel": "Elasticsearch 集群告警", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "此集群的许可证将于 #relative后,即 #absolute到期。 #start_link请更新您的许可证。#end_link", "xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth": "此集群中运行的 Logstash 版本。", "xpack.monitoring.alerts.logstashVersionMismatch.description": "集群包含多个版本的 Logstash 时告警。", @@ -14542,7 +14538,6 @@ "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Logstash 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.logstashVersionMismatch.fullAction": "查看节点", "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash 版本不匹配", - "xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel": "Logstash 节点告警", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Logstash 版本 ({versions})。", "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "报告高内存使用率的节点数目。", @@ -14585,7 +14580,6 @@ "xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage": "为 {clusterName} 触发了节点已更改告警。{shortActionText}", "xpack.monitoring.alerts.nodesChanged.fullAction": "查看节点", "xpack.monitoring.alerts.nodesChanged.label": "节点已更改", - "xpack.monitoring.alerts.nodesChanged.nodeNameLabel": "Elasticsearch 节点告警", "xpack.monitoring.alerts.nodesChanged.shortAction": "确认您已添加、移除或重新启动节点。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearch 节点“{added}”已添加到此集群。", "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearch 节点已更改", From c7e23bffc681805460b6b2531723cae0d0d7b79e Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 8 Feb 2021 21:51:42 -0500 Subject: [PATCH 036/100] [Uptime] Migrate to TypeScript project references (#90510) * Add reference to Uptime plugin to root tsconfig.refs.json. * Add Uptime path to excluded list, and reference to references prop in `x-pack/tsconfig.json`. * Add reference to Uptime project in `x-pack/test/tsconfig.json`. * Add `tsconfig.json` project file to Uptime. * Fix broken JSON structure in test fixture. * Fix broken type exports. Introduce missing types. * Implement PR feedback. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/uptime/common/constants/alerts.ts | 40 ++++++++++++------- .../synthetics/waterfall/components/styles.ts | 16 ++++++-- .../actions_popover/actions_popover.tsx | 2 +- .../monitor_list_drawer/data.json | 20 +++++++++- .../uptime/public/state/alerts/alerts.ts | 2 +- .../public/state/certificates/certificates.ts | 2 +- x-pack/plugins/uptime/public/state/index.ts | 7 ++-- .../uptime/public/state/reducers/journey.ts | 2 +- .../server/lib/alerts/duration_anomaly.ts | 3 +- .../uptime/server/lib/alerts/status_check.ts | 3 +- .../plugins/uptime/server/lib/alerts/tls.ts | 3 +- x-pack/plugins/uptime/server/lib/lib.ts | 2 +- .../lib/requests/get_journey_details.ts | 2 +- .../lib/requests/get_journey_failed_steps.ts | 2 +- .../lib/requests/get_journey_screenshot.ts | 2 +- .../server/lib/requests/get_journey_steps.ts | 2 +- .../server/lib/requests/get_network_events.ts | 2 +- .../uptime/server/lib/requests/helper.ts | 15 +++++-- x-pack/plugins/uptime/tsconfig.json | 24 +++++++++++ x-pack/test/tsconfig.json | 6 ++- x-pack/tsconfig.json | 4 +- x-pack/tsconfig.refs.json | 3 +- 22 files changed, 117 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/uptime/tsconfig.json diff --git a/x-pack/plugins/uptime/common/constants/alerts.ts b/x-pack/plugins/uptime/common/constants/alerts.ts index a79a00c54c3de..46ce0b27e406b 100644 --- a/x-pack/plugins/uptime/common/constants/alerts.ts +++ b/x-pack/plugins/uptime/common/constants/alerts.ts @@ -7,23 +7,33 @@ import { ActionGroup } from '../../../alerts/common'; +export type MonitorStatusActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.monitorStatus'>; +export type TLSActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; +export type DurationAnomalyActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.durationAnomaly'>; + +export const MONITOR_STATUS: MonitorStatusActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.monitorStatus', + name: 'Uptime Down Monitor', +}; + +export const TLS: TLSActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.tls', + name: 'Uptime TLS Alert', +}; + +export const DURATION_ANOMALY: DurationAnomalyActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.durationAnomaly', + name: 'Uptime Duration Anomaly', +}; + export const ACTION_GROUP_DEFINITIONS: { - MONITOR_STATUS: ActionGroup<'xpack.uptime.alerts.actionGroups.monitorStatus'>; - TLS: ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; - DURATION_ANOMALY: ActionGroup<'xpack.uptime.alerts.actionGroups.durationAnomaly'>; + MONITOR_STATUS: MonitorStatusActionGroup; + TLS: TLSActionGroup; + DURATION_ANOMALY: DurationAnomalyActionGroup; } = { - MONITOR_STATUS: { - id: 'xpack.uptime.alerts.actionGroups.monitorStatus', - name: 'Uptime Down Monitor', - }, - TLS: { - id: 'xpack.uptime.alerts.actionGroups.tls', - name: 'Uptime TLS Alert', - }, - DURATION_ANOMALY: { - id: 'xpack.uptime.alerts.actionGroups.durationAnomaly', - name: 'Uptime Duration Anomaly', - }, + MONITOR_STATUS, + TLS, + DURATION_ANOMALY, }; export const CLIENT_ALERT_TYPES = { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index c00c04b114045..9177902f8a613 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiPanelProps } from '@elastic/eui'; import { rgba } from 'polished'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; +import { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; import { FIXED_AXIS_HEIGHT } from './constants'; interface WaterfallChartOuterContainerProps { @@ -53,7 +55,10 @@ export const WaterfallChartAxisOnlyContainer = euiStyled(EuiFlexItem)` export const WaterfallChartTopContainer = euiStyled(EuiFlexGroup)` `; -export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` +export const WaterfallChartFixedTopContainerSidebarCover: StyledComponent< + FunctionComponent, + EuiTheme +> = euiStyled(EuiPanel)` height: 100%; border-radius: 0 !important; border: none; @@ -82,7 +87,10 @@ export const WaterfallChartSidebarContainer = euiStyled.div, + EuiTheme +> = euiStyled(EuiPanel)` border: 0; height: 100%; `; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx index ecc6231ba05fd..9ee6dc749b9eb 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx @@ -12,7 +12,7 @@ import { IntegrationGroup } from './integration_group'; import { MonitorSummary } from '../../../../../../common/runtime_types'; import { toggleIntegrationsPopover, PopoverState } from '../../../../../state/actions'; -interface ActionsPopoverProps { +export interface ActionsPopoverProps { summary: MonitorSummary; popoverState: PopoverState | null; togglePopoverIsVisible: typeof toggleIntegrationsPopover; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json index 1bbdcd4a30078..905e982681dee 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json @@ -261,7 +261,25 @@ }, "state": { "agent": null, - "checks": , + "checks": [ + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "127.0.0.1", + "name": "localhost", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246143", + "__typename": "Check" + } + ], "geo": null, "observer": { "geo": { "name": [], "location": null, "__typename": "StateGeo" }, diff --git a/x-pack/plugins/uptime/public/state/alerts/alerts.ts b/x-pack/plugins/uptime/public/state/alerts/alerts.ts index 4b48b157c3deb..f328bd5b9a5a7 100644 --- a/x-pack/plugins/uptime/public/state/alerts/alerts.ts +++ b/x-pack/plugins/uptime/public/state/alerts/alerts.ts @@ -53,7 +53,7 @@ export const deleteAnomalyAlertAction = createAsyncAction<{ alertId: string }, a 'DELETE ANOMALY ALERT' ); -interface AlertState { +export interface AlertState { connectors: AsyncInitState; newAlert: AsyncInitState>; alerts: AsyncInitState; diff --git a/x-pack/plugins/uptime/public/state/certificates/certificates.ts b/x-pack/plugins/uptime/public/state/certificates/certificates.ts index d6d48f2ab7007..ca2d5e7a17a46 100644 --- a/x-pack/plugins/uptime/public/state/certificates/certificates.ts +++ b/x-pack/plugins/uptime/public/state/certificates/certificates.ts @@ -19,7 +19,7 @@ export const getCertificatesAction = createAsyncAction; } diff --git a/x-pack/plugins/uptime/public/state/index.ts b/x-pack/plugins/uptime/public/state/index.ts index fa15e77f7fcc4..61b1a5f9d9527 100644 --- a/x-pack/plugins/uptime/public/state/index.ts +++ b/x-pack/plugins/uptime/public/state/index.ts @@ -5,17 +5,16 @@ * 2.0. */ -import { compose, createStore, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware } from 'redux'; +import { composeWithDevTools } from 'redux-devtools-extension'; import createSagaMiddleware from 'redux-saga'; import { rootEffect } from './effects'; import { rootReducer } from './reducers'; export type AppState = ReturnType; -const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - const sagaMW = createSagaMiddleware(); -export const store = createStore(rootReducer, composeEnhancers(applyMiddleware(sagaMW))); +export const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(sagaMW))); sagaMW.run(rootEffect); diff --git a/x-pack/plugins/uptime/public/state/reducers/journey.ts b/x-pack/plugins/uptime/public/state/reducers/journey.ts index 273523f4592d6..361454e1b3fa1 100644 --- a/x-pack/plugins/uptime/public/state/reducers/journey.ts +++ b/x-pack/plugins/uptime/public/state/reducers/journey.ts @@ -24,7 +24,7 @@ export interface JourneyState { error?: Error; } -interface JourneyKVP { +export interface JourneyKVP { [checkGroup: string]: JourneyState; } diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 6310b79206a88..0c9f9dd849341 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -10,7 +10,7 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; import { ActionGroupIdsOf } from '../../../../alerts/common'; import { updateState } from './common'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { DURATION_ANOMALY } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies'; import { getSeverityType } from '../../../../ml/common/util/anomaly_utils'; @@ -21,7 +21,6 @@ import { getMLJobId } from '../../../common/lib'; import { getLatestMonitor } from '../requests/get_latest_monitor'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; -const { DURATION_ANOMALY } = ACTION_GROUP_DEFINITIONS; export type ActionGroupIds = ActionGroupIdsOf; export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Ping) => { diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index cc1cb3a4ed0be..cee20d113c256 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -17,7 +17,7 @@ import { Ping, GetMonitorAvailabilityParams, } from '../../../common/runtime_types'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { MONITOR_STATUS } from '../../../common/constants/alerts'; import { updateState } from './common'; import { commonMonitorStateI18, commonStateTranslations, DOWN_LABEL } from './translations'; import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib'; @@ -29,7 +29,6 @@ import { MonitorStatusTranslations } from '../../../common/translations'; import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern'; import { UMServerLibs, UptimeESClient } from '../lib'; -const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; export type ActionGroupIds = ActionGroupIdsOf; const getMonIdByLoc = (monitorId: string, location: string) => { diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 345d2470ed705..7bc4c36b98e8b 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; import { UptimeAlertTypeFactory } from './types'; import { updateState } from './common'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { TLS } from '../../../common/constants/alerts'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { Cert, CertResult } from '../../../common/runtime_types'; import { commonStateTranslations, tlsTranslations } from './translations'; @@ -17,7 +17,6 @@ import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; import { ActionGroupIdsOf } from '../../../../alerts/common'; -const { TLS } = ACTION_GROUP_DEFINITIONS; export type ActionGroupIds = ActionGroupIdsOf; const DEFAULT_SIZE = 20; diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index 53a79815a0c0f..5ac56d14c171d 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -27,7 +27,7 @@ export interface UMServerLibs extends UMDomainLibs { framework: UMBackendFrameworkAdapter; } -interface CountResponse { +export interface CountResponse { body: { count: number; _shards: { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts index c942c3a8f69fd..e0edcc4576378 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types'; -interface GetJourneyDetails { +export interface GetJourneyDetails { checkGroup: string; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts index 1abba0087cb44..9865bd95fe961 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types'; -interface GetJourneyStepsParams { +export interface GetJourneyStepsParams { checkGroups: string[]; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts index ff9aec85e28bb..9cb5e1eedb6b0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types/ping'; -interface GetJourneyScreenshotParams { +export interface GetJourneyScreenshotParams { checkGroup: string; stepIndex: number; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index d657b8b9aacf3..3055f169fc495 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types'; -interface GetJourneyStepsParams { +export interface GetJourneyStepsParams { checkGroup: string; syntheticEventTypes?: string | string[]; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index f9936c6f273ba..fa76da0025305 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { NetworkEvent } from '../../../common/runtime_types'; -interface GetNetworkEventsParams { +export interface GetNetworkEventsParams { checkGroup: string; stepIndex: string; } diff --git a/x-pack/plugins/uptime/server/lib/requests/helper.ts b/x-pack/plugins/uptime/server/lib/requests/helper.ts index 2556d7b8fb8cd..e3969f84c8485 100644 --- a/x-pack/plugins/uptime/server/lib/requests/helper.ts +++ b/x-pack/plugins/uptime/server/lib/requests/helper.ts @@ -5,14 +5,14 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ElasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { elasticsearchServiceMock, savedObjectsClientMock, } from '../../../../../../src/core/server/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ElasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; -import { createUptimeESClient } from '../lib'; +import { createUptimeESClient, UptimeESClient } from '../lib'; export interface MultiPageCriteria { after_key?: K; @@ -60,7 +60,14 @@ export const setupMockEsCompositeQuery = ( return esMock; }; -export const getUptimeESMockClient = (esClientMock?: ElasticsearchClientMock) => { +interface UptimeEsMockClient { + esClient: ElasticsearchClientMock; + uptimeEsClient: UptimeESClient; +} + +export const getUptimeESMockClient = ( + esClientMock?: ElasticsearchClientMock +): UptimeEsMockClient => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); const savedObjectsClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/uptime/tsconfig.json b/x-pack/plugins/uptime/tsconfig.json new file mode 100644 index 0000000000000..5a195f6c2df25 --- /dev/null +++ b/x-pack/plugins/uptime/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json", + "server/**/*", + "server/lib/requests/__fixtures__/monitor_charts_mock.json", + "../../../typings/**/*" + ], + "references": [ + { "path": "../alerts/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" }, + { "path": "../observability/tsconfig.json" } + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 0a7a30f373e07..2981346e80e1d 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -76,6 +76,10 @@ { "path": "../plugins/triggers_actions_ui/tsconfig.json" }, { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../plugins/upgrade_assistant/tsconfig.json" }, - { "path": "../plugins/watcher/tsconfig.json" } + { "path": "../plugins/watcher/tsconfig.json" }, + { "path": "../plugins/runtime_fields/tsconfig.json" }, + { "path": "../plugins/index_management/tsconfig.json" }, + { "path": "../plugins/watcher/tsconfig.json" }, + { "path": "../plugins/uptime/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 5d51c2923abd0..740bac3f1b0de 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -56,6 +56,7 @@ "plugins/index_management/**/*", "plugins/grokdebugger/**/*", "plugins/upgrade_assistant/**/*", + "plugins/uptime/**/*", "test/**/*" ], "compilerOptions": { @@ -145,6 +146,7 @@ { "path": "./plugins/upgrade_assistant/tsconfig.json" }, { "path": "./plugins/runtime_fields/tsconfig.json" }, { "path": "./plugins/index_management/tsconfig.json" }, - { "path": "./plugins/watcher/tsconfig.json" } + { "path": "./plugins/watcher/tsconfig.json" }, + { "path": "./plugins/uptime/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index ae88ab6486e64..7a2eebc78b69b 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -50,6 +50,7 @@ { "path": "./plugins/upgrade_assistant/tsconfig.json" }, { "path": "./plugins/runtime_fields/tsconfig.json" }, { "path": "./plugins/index_management/tsconfig.json" }, - { "path": "./plugins/watcher/tsconfig.json" } + { "path": "./plugins/watcher/tsconfig.json" }, + { "path": "./plugins/uptime/tsconfig.json" } ] } From a997178c0e8982322bffe1e79b2eab6ce1d39e77 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 9 Feb 2021 08:51:49 +0200 Subject: [PATCH 037/100] Fix vega renovate label (#90591) --- renovate.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json5 b/renovate.json5 index 1585627daa880..f1e773427a103 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -57,7 +57,7 @@ groupName: 'vega related modules', packageNames: ['vega', 'vega-lite', 'vega-schema-url-parser', 'vega-tooltip'], reviewers: ['team:kibana-app'], - labels: ['Feature:Lens', 'Team:KibanaApp'], + labels: ['Feature:Vega', 'Team:KibanaApp'], enabled: true, }, ], From 451d0819bcc12c54415ca21afed50abac01d46c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 9 Feb 2021 08:26:36 +0100 Subject: [PATCH 038/100] Strongly typed EUI theme for styled-components (#90106) * Strongly typed EUI theme for styled-components use euiStyled fix tsc issue * use relative imports * remove redundant types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/public/application/csmApp.tsx | 5 +++-- x-pack/plugins/apm/public/application/index.tsx | 5 +++-- .../app/ErrorGroupDetails/DetailView/index.tsx | 6 +++--- .../components/app/ErrorGroupDetails/index.tsx | 10 +++++----- .../LocalUIFilters/Filter/FilterBadgeList.tsx | 4 ++-- .../LocalUIFilters/Filter/FilterTitleButton.tsx | 4 ++-- .../RumDashboard/LocalUIFilters/Filter/index.tsx | 12 ++++++------ .../app/RumDashboard/LocalUIFilters/index.tsx | 4 ++-- .../components/app/ServiceMap/Controls.tsx | 10 +++++----- .../components/app/ServiceMap/EmptyBanner.tsx | 4 ++-- .../app/ServiceMap/Popover/AnomalyDetection.tsx | 12 ++++++------ .../components/app/ServiceMap/Popover/Info.tsx | 8 ++++---- .../app/ServiceMap/Popover/ServiceStatsList.tsx | 8 ++++---- .../public/components/app/ServiceMap/index.tsx | 4 ++-- .../public/components/app/TraceLink/index.tsx | 4 ++-- .../app/error_group_overview/List/index.tsx | 12 ++++++------ .../app/service_inventory/ServiceList/index.tsx | 6 +++--- .../app/service_node_metrics/index.tsx | 6 +++--- .../app/service_node_overview/index.tsx | 4 ++-- .../service_overview_table_container.tsx | 4 ++-- .../components/app/trace_overview/TraceList.tsx | 4 ++-- .../WaterfallContainer/ServiceLegends.tsx | 4 ++-- .../Waterfall/ResponsiveFlyout.tsx | 5 ++--- .../Waterfall/SpanFlyout/DatabaseContext.tsx | 4 ++-- .../Waterfall/SpanFlyout/HttpContext.tsx | 5 ++--- .../SpanFlyout/TruncateHeightSection.tsx | 4 ++-- .../Waterfall/SpanFlyout/index.tsx | 8 ++++---- .../WaterfallContainer/Waterfall/SyncBadge.tsx | 6 +++--- .../Waterfall/WaterfallItem.tsx | 8 ++++---- .../Waterfall/accordion_waterfall.tsx | 6 +++--- .../WaterfallContainer/Waterfall/index.tsx | 6 +++--- .../TransactionList/index.tsx | 4 ++-- .../public/components/shared/ApmHeader/index.tsx | 4 ++-- .../shared/KeyValueTable/FormattedValue.tsx | 4 ++-- .../shared/KueryBar/Typeahead/Suggestion.js | 10 +++++----- .../shared/KueryBar/Typeahead/Suggestions.js | 4 ++-- .../public/components/shared/KueryBar/index.tsx | 4 ++-- .../shared/Stacktrace/CauseStacktrace.tsx | 10 +++++----- .../components/shared/Stacktrace/Context.tsx | 16 ++++++++-------- .../shared/Stacktrace/FrameHeading.tsx | 8 ++++---- .../shared/Stacktrace/LibraryStacktrace.tsx | 4 ++-- .../components/shared/Stacktrace/Stackframe.tsx | 6 +++--- .../components/shared/Stacktrace/Variables.tsx | 4 ++-- .../components/shared/StickyProperties/index.tsx | 10 +++++----- .../Summary/ErrorCountSummaryItemBadge.tsx | 6 +++--- .../shared/Summary/HttpInfoSummaryItem/index.tsx | 10 +++++----- .../shared/Summary/UserAgentSummaryItem.tsx | 4 ++-- .../public/components/shared/Summary/index.tsx | 4 ++-- .../components/shared/charts/Legend/index.tsx | 6 +++--- .../charts/Timeline/Marker/AgentMarker.tsx | 6 +++--- .../charts/Timeline/Marker/ErrorMarker.tsx | 10 +++++----- .../shared/charts/Timeline/Marker/index.tsx | 4 ++-- .../charts/transaction_charts/ml_header.tsx | 6 +++--- .../apm/public/components/shared/main_tabs.tsx | 4 ++-- .../apm/public/components/shared/search_bar.tsx | 4 ++-- .../components/shared/time_comparison/index.tsx | 4 ++-- .../shared/truncate_with_tooltip/index.tsx | 6 +++--- 57 files changed, 177 insertions(+), 177 deletions(-) diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 5fdd45336eb72..8ea4593bb89a7 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -11,7 +11,8 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router } from 'react-router-dom'; -import styled, { DefaultTheme, ThemeProvider } from 'styled-components'; +import { DefaultTheme, ThemeProvider } from 'styled-components'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider, RedirectAppLinks, @@ -30,7 +31,7 @@ import { createCallApmApi } from '../services/rest/createCallApmApi'; import { px, units } from '../style/variables'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; -const CsmMainContainer = styled.div` +const CsmMainContainer = euiStyled.div` padding: ${px(units.plus)}; height: 100%; `; diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 1996cf3bfe2d9..0028b392fc838 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -12,7 +12,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import 'react-vis/dist/style.css'; -import styled, { DefaultTheme, ThemeProvider } from 'styled-components'; +import { DefaultTheme, ThemeProvider } from 'styled-components'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { ConfigSchema } from '../'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; import { @@ -35,7 +36,7 @@ import { createStaticIndexPattern } from '../services/rest/index_pattern'; import { setHelpExtension } from '../setHelpExtension'; import { setReadonlyBadge } from '../updateBadge'; -const MainContainer = styled.div` +const MainContainer = euiStyled.div` height: 100%; `; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index ebd15262fd089..cd893c1736988 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -20,7 +20,7 @@ import { Location } from 'history'; import { first } from 'lodash'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import type { IUrlParams } from '../../../../context/url_params_context/types'; @@ -42,14 +42,14 @@ import { } from './ErrorTabs'; import { ExceptionStacktrace } from './ExceptionStacktrace'; -const HeaderContainer = styled.div` +const HeaderContainer = euiStyled.div` display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: ${px(unit)}; `; -const TransactionLinkName = styled.div` +const TransactionLinkName = euiStyled.div` margin-left: ${px(units.half)}; display: inline-block; vertical-align: middle; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index dfc5986f88228..9a8c2dffacaf7 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTrackPageview } from '../../../../../observability/public'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; import { useFetcher } from '../../../hooks/use_fetcher'; @@ -31,24 +31,24 @@ import { DetailView } from './DetailView'; import { ErrorDistribution } from './Distribution'; import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; -const Titles = styled.div` +const Titles = euiStyled.div` margin-bottom: ${px(units.plus)}; `; -const Label = styled.div` +const Label = euiStyled.div` margin-bottom: ${px(units.quarter)}; font-size: ${fontSizes.small}; color: ${({ theme }) => theme.eui.euiColorMediumShade}; `; -const Message = styled.div` +const Message = euiStyled.div` font-family: ${fontFamilyCode}; font-weight: bold; font-size: ${fontSizes.large}; margin-bottom: ${px(units.half)}; `; -const Culprit = styled.div` +const Culprit = euiStyled.div` font-family: ${fontFamilyCode}; `; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx index 6bc345ea5bd87..785d50de64553 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '@elastic/eui'; -import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { unit, px, truncate } from '../../../../../style/variables'; -const BadgeText = styled.div` +const BadgeText = euiStyled.div` display: inline-block; ${truncate(px(unit * 8))}; vertical-align: middle; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx index 5d60f7c2aa332..1a59b7d910b1f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { EuiButtonEmpty, EuiTitle } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -const Button = styled(EuiButtonEmpty).attrs(() => ({ +const Button = euiStyled(EuiButtonEmpty).attrs(() => ({ contentProps: { className: 'alignLeft', }, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx index e1debde1117f9..391766a0cf927 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx @@ -19,12 +19,12 @@ import { EuiFlexGroup, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { FilterBadgeList } from './FilterBadgeList'; import { unit, px } from '../../../../../style/variables'; import { FilterTitleButton } from './FilterTitleButton'; -const Popover = styled((EuiPopover as unknown) as FunctionComponent).attrs( +const Popover = euiStyled((EuiPopover as unknown) as FunctionComponent).attrs( () => ({ anchorClassName: 'anchor', }) @@ -34,22 +34,22 @@ const Popover = styled((EuiPopover as unknown) as FunctionComponent).attrs( } `; -const SelectContainer = styled.div` +const SelectContainer = euiStyled.div` width: ${px(unit * 16)}; `; -const Counter = styled.div` +const Counter = euiStyled.div` border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; background: ${({ theme }) => theme.eui.euiColorLightShade}; padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs}; `; -const ApplyButton = styled(EuiButton)` +const ApplyButton = euiStyled(EuiButton)` align-self: flex-end; `; // needed for IE11 -const FlexItem = styled(EuiFlexItem)` +const FlexItem = euiStyled(EuiFlexItem)` flex-basis: auto !important; `; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx index a07997fb74921..4afecb7623f73 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx @@ -13,7 +13,7 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../hooks/useLocalUIFilters'; import { LocalUIFilterName } from '../../../../../common/ui_filter'; @@ -26,7 +26,7 @@ interface Props { shouldFetch?: boolean; } -const ButtonWrapper = styled.div` +const ButtonWrapper = euiStyled.div` display: inline-block; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx index 9737f6a5e2eba..3362219fd5f2d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -8,7 +8,7 @@ import { EuiButtonIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext, useEffect, useState } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useTheme } from '../../../hooks/use_theme'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -17,23 +17,23 @@ import { APMQueryParams } from '../../shared/Links/url_helpers'; import { CytoscapeContext } from './Cytoscape'; import { getAnimationOptions, getNodeHeight } from './cytoscape_options'; -const ControlsContainer = styled('div')` +const ControlsContainer = euiStyled('div')` left: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; position: absolute; top: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; z-index: 1; /* The element containing the cytoscape canvas has z-index = 0. */ `; -const Button = styled(EuiButtonIcon)` +const Button = euiStyled(EuiButtonIcon)` display: block; margin: ${({ theme }) => theme.eui.paddingSizes.xs}; `; -const ZoomInButton = styled(Button)` +const ZoomInButton = euiStyled(Button)` margin-bottom: ${({ theme }) => theme.eui.paddingSizes.s}; `; -const Panel = styled(EuiPanel)` +const Panel = euiStyled(EuiPanel)` margin-bottom: ${({ theme }) => theme.eui.paddingSizes.s}; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx index 0cbf3f013f148..90caa9c87c484 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx @@ -8,12 +8,12 @@ import React, { useContext, useEffect, useState } from 'react'; import { EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { CytoscapeContext } from './Cytoscape'; import { useTheme } from '../../../hooks/use_theme'; -const EmptyBannerContainer = styled.div` +const EmptyBannerContainer = euiStyled.div` margin: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; /* Add some extra margin so it displays to the right of the controls. */ left: calc( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index 50b1502a86fd3..c98116a69da66 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, @@ -15,6 +14,7 @@ import { EuiIconTip, EuiHealth, } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { getServiceHealthStatus, getServiceHealthStatusColor, @@ -30,28 +30,28 @@ import { ServiceAnomalyStats, } from '../../../../../common/anomaly_detection'; -const HealthStatusTitle = styled(EuiTitle)` +const HealthStatusTitle = euiStyled(EuiTitle)` display: inline; text-transform: uppercase; `; -const VerticallyCentered = styled.div` +const VerticallyCentered = euiStyled.div` display: flex; align-items: center; `; -const SubduedText = styled.span` +const SubduedText = euiStyled.span` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; `; -const EnableText = styled.section` +const EnableText = euiStyled.section` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; line-height: 1.4; font-size: ${fontSize}; width: ${px(popoverWidth)}; `; -export const ContentLine = styled.section` +export const ContentLine = euiStyled.section` line-height: 2; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 4900d1dedbde5..9577a02d68cf2 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -13,24 +13,24 @@ import { import { i18n } from '@kbn/i18n'; import cytoscape from 'cytoscape'; import React, { Fragment } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { SPAN_SUBTYPE, SPAN_TYPE, } from '../../../../../common/elasticsearch_fieldnames'; import { ExternalConnectionNode } from '../../../../../common/service_map'; -const ItemRow = styled.div` +const ItemRow = euiStyled.div` line-height: 2; `; -const SubduedDescriptionListTitle = styled(EuiDescriptionListTitle)` +const SubduedDescriptionListTitle = euiStyled(EuiDescriptionListTitle)` &&& { color: ${({ theme }) => theme.eui.euiTextSubduedColor}; } `; -const ExternalResourcesList = styled.section` +const ExternalResourcesList = euiStyled.section` max-height: 360px; overflow: auto; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx index 65508e6adc0ca..766debc6d5587 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { asDuration, asPercent, @@ -16,16 +16,16 @@ import { } from '../../../../../common/utils/formatters'; import { ServiceNodeStats } from '../../../../../common/service_map'; -export const ItemRow = styled('tr')` +export const ItemRow = euiStyled('tr')` line-height: 2; `; -export const ItemTitle = styled('td')` +export const ItemTitle = euiStyled('td')` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; padding-right: 1rem; `; -export const ItemDescription = styled('td')` +export const ItemDescription = euiStyled('td')` text-align: right; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 7021575da905e..7ef3cbca3ad2f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import React, { PropsWithChildren, ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { useTrackPageview } from '../../../../../observability/public'; import { @@ -33,7 +33,7 @@ interface ServiceMapProps { serviceName?: string; } -const ServiceMapDatePickerFlexGroup = styled(EuiFlexGroup)` +const ServiceMapDatePickerFlexGroup = euiStyled(EuiFlexGroup)` padding: ${({ theme }) => theme.eui.euiSizeM}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; margin: 0; diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index 302b815f78715..d0c2b5c598039 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -8,13 +8,13 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { getRedirectToTransactionDetailPageUrl } from './get_redirect_to_transaction_detail_page_url'; import { getRedirectToTracePageUrl } from './get_redirect_to_trace_page_url'; -const CentralizedContainer = styled.div` +const CentralizedContainer = euiStyled.div` height: 100%; display: flex; `; diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx index 9d891151e75d2..66fb72975acea 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx @@ -9,8 +9,8 @@ import { EuiBadge, EuiToolTip } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; import { EuiIconTip } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { @@ -27,25 +27,25 @@ import { TimestampTooltip } from '../../../shared/TimestampTooltip'; import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; import { APMQueryParams } from '../../../shared/Links/url_helpers'; -const GroupIdLink = styled(ErrorDetailLink)` +const GroupIdLink = euiStyled(ErrorDetailLink)` font-family: ${fontFamilyCode}; `; -const MessageAndCulpritCell = styled.div` +const MessageAndCulpritCell = euiStyled.div` ${truncate('100%')}; `; -const ErrorLink = styled(ErrorOverviewLink)` +const ErrorLink = euiStyled(ErrorOverviewLink)` ${truncate('100%')}; `; -const MessageLink = styled(ErrorDetailLink)` +const MessageLink = euiStyled(ErrorDetailLink)` font-family: ${fontFamilyCode}; font-size: ${fontSizes.large}; ${truncate('100%')}; `; -const Culprit = styled.div` +const Culprit = euiStyled.div` font-family: ${fontFamilyCode}; `; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx index 4506700380390..5287e6699aaee 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx @@ -8,11 +8,11 @@ import { EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; import { EuiIcon } from '@elastic/eui'; import { EuiText } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, @@ -46,12 +46,12 @@ function formatString(value?: string | null) { return value || NOT_AVAILABLE_LABEL; } -const AppLink = styled(ServiceOrTransactionsOverviewLink)` +const AppLink = euiStyled(ServiceOrTransactionsOverviewLink)` font-size: ${fontSizes.large}; ${truncate('100%')}; `; -const ToolTipWrapper = styled.span` +const ToolTipWrapper = euiStyled.span` width: 100%; .apmServiceList__serviceNameTooltip { width: 100%; diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index 5832f2b7d1ac9..21871a17f4b04 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; @@ -39,12 +39,12 @@ const INITIAL_DATA = { containerId: '', }; -const Truncate = styled.span` +const Truncate = euiStyled.span` display: block; ${truncate(px(unit * 12))} `; -const MetadataFlexGroup = styled(EuiFlexGroup)` +const MetadataFlexGroup = euiStyled(EuiFlexGroup)` border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; margin-bottom: ${({ theme }) => theme.eui.paddingSizes.m}; padding: ${({ theme }) => diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 00d184f692e3b..c64bbcb569dde 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiPage, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { @@ -26,7 +26,7 @@ const INITIAL_PAGE_SIZE = 25; const INITIAL_SORT_FIELD = 'cpu'; const INITIAL_SORT_DIRECTION = 'desc'; -const ServiceNodeName = styled.div` +const ServiceNodeName = euiStyled.div` ${truncate(px(8 * unit))} `; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx index 45d34cd304ce7..738ff0d7c735f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx @@ -6,7 +6,7 @@ */ import React, { ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useBreakPoints } from '../../../hooks/use_break_points'; /** @@ -24,7 +24,7 @@ const tableHeight = 282; * * Hide the empty message when we don't yet have any items and are still loading. */ -const ServiceOverviewTableContainerDiv = styled.div<{ +const ServiceOverviewTableContainerDiv = euiStyled.div<{ isEmptyAndLoading: boolean; shouldUseMobileLayout: boolean; }>` diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx index cdb82418180ba..774333c35b479 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx @@ -8,7 +8,7 @@ import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { asMillisecondDuration, asTransactionRate, @@ -23,7 +23,7 @@ import { APIReturnType } from '../../../services/rest/createCallApmApi'; type TraceGroup = APIReturnType<'GET /api/apm/traces'>['items'][0]; -const StyledTransactionLink = styled(TransactionDetailLink)` +const StyledTransactionLink = euiStyled(TransactionDetailLink)` font-size: ${fontSizes.large}; ${truncate('100%')}; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx index 2f4c3e3a9d24c..ab3773b2cac2e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx @@ -8,12 +8,12 @@ import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { px, unit } from '../../../../../style/variables'; import { Legend } from '../../../../shared/charts/Legend'; import { IServiceColors } from './Waterfall/waterfall_helpers/waterfall_helpers'; -const Legends = styled.div` +const Legends = euiStyled.div` display: flex; > * { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx index 2812c686d7121..8549f09bba248 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx @@ -6,10 +6,9 @@ */ import { EuiFlyout } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; -import styled from 'styled-components'; - -export const ResponsiveFlyout = styled(EuiFlyout)` +export const ResponsiveFlyout = euiStyled(EuiFlyout)` width: 100%; @media (min-width: 800px) { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx index 3509500d9f429..fda2d595e669d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx @@ -12,7 +12,7 @@ import React, { Fragment } from 'react'; import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; import xcode from 'react-syntax-highlighter/dist/cjs/styles/hljs/xcode'; import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { borderRadius, @@ -26,7 +26,7 @@ import { TruncateHeightSection } from './TruncateHeightSection'; SyntaxHighlighter.registerLanguage('sql', sql); -const DatabaseStatement = styled.div` +const DatabaseStatement = euiStyled.div` padding: ${px(units.half)} ${px(unit)}; background: ${({ theme }) => tint(0.1, theme.eui.euiColorWarning)}; border-radius: ${borderRadius}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx index 065dadc6dfd0d..3584309ebb20c 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx @@ -6,9 +6,8 @@ */ import React, { Fragment } from 'react'; -import styled from 'styled-components'; - import { EuiSpacer, EuiTitle } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { borderRadius, fontFamilyCode, @@ -19,7 +18,7 @@ import { } from '../../../../../../../style/variables'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; -const ContextUrl = styled.div` +const ContextUrl = euiStyled.div` padding: ${px(units.half)} ${px(unit)}; background: ${({ theme }) => theme.eui.euiColorLightestShade}; border-radius: ${borderRadius}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx index 401c34ed32436..181fcb91ba3e6 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx @@ -8,10 +8,10 @@ import { EuiIcon, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment, ReactNode, useEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../../../../style/variables'; -const ToggleButtonContainer = styled.div` +const ToggleButtonContainer = euiStyled.div` margin-top: ${px(units.half)}; user-select: none; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index 35f71676da20e..fe4384e84427f 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../../../../style/variables'; import { Summary } from '../../../../../../shared/Summary'; import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; @@ -72,12 +72,12 @@ function getSpanTypes(span: Span) { }; } -const SpanBadge = (styled(EuiBadge)` +const SpanBadge = euiStyled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -` as unknown) as typeof EuiBadge; +`; -const HttpInfoContainer = styled('div')` +const HttpInfoContainer = euiStyled('div')` margin-right: ${px(units.quarter)}; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx index cfc90741b0469..24301b2cf10fb 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx @@ -8,13 +8,13 @@ import { EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../../../style/variables'; -const SpanBadge = (styled(EuiBadge)` +const SpanBadge = euiStyled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -` as unknown) as typeof EuiBadge; +`; interface SyncBadgeProps { /** diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index eb34b457d756d..7000f389e3d0e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -6,10 +6,10 @@ */ import React, { ReactNode } from 'react'; -import styled from 'styled-components'; import { EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../../common/utils/formatters'; import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; @@ -33,7 +33,7 @@ interface IBarStyleProps { color: string; } -const Container = styled.div` +const Container = euiStyled.div` position: relative; display: block; user-select: none; @@ -50,7 +50,7 @@ const Container = styled.div` } `; -const ItemBar = styled.div` +const ItemBar = euiStyled.div` box-sizing: border-box; position: relative; height: ${px(unit)}; @@ -58,7 +58,7 @@ const ItemBar = styled.div` background-color: ${(props) => props.color}; `; -const ItemText = styled.span` +const ItemText = euiStyled.span` position: absolute; right: 0; display: flex; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx index 08bd8c21b7649..8d50074d814eb 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx @@ -9,7 +9,7 @@ import { EuiAccordion, EuiAccordionProps } from '@elastic/eui'; import { Location } from 'history'; import { isEmpty } from 'lodash'; import React, { useState } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { Margins } from '../../../../../shared/charts/Timeline'; import { WaterfallItem } from './WaterfallItem'; import { @@ -32,7 +32,7 @@ interface AccordionWaterfallProps { onClickWaterfallItem: (item: IWaterfallItem) => void; } -const StyledAccordion = styled(EuiAccordion).withConfig({ +const StyledAccordion = euiStyled(EuiAccordion).withConfig({ shouldForwardProp: (prop) => !['childrenCount', 'marginLeftLevel', 'hasError'].includes(prop), })< @@ -86,7 +86,7 @@ const StyledAccordion = styled(EuiAccordion).withConfig({ }} `; -const WaterfallItemContainer = styled.div` +const WaterfallItemContainer = euiStyled.div` position: absolute; width: 100%; left: 0; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index 2ee3b53242a78..a680fdc404402 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { History, Location } from 'history'; import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { Timeline } from '../../../../../shared/charts/Timeline'; import { HeightRetainer } from '../../../../../shared/HeightRetainer'; import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; @@ -23,7 +23,7 @@ import { IWaterfallItem, } from './waterfall_helpers/waterfall_helpers'; -const Container = styled.div` +const Container = euiStyled.div` transition: 0.1s padding ease; position: relative; overflow: hidden; @@ -55,7 +55,7 @@ const toggleFlyout = ({ }); }; -const WaterfallItemsContainer = styled.div` +const WaterfallItemsContainer = euiStyled.div` border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx index 9a1a691154b18..795a6e66f70a4 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx @@ -8,7 +8,7 @@ import { EuiToolTip, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { @@ -26,7 +26,7 @@ type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/trans // Truncate both the link and the child span (the tooltip anchor.) The link so // it doesn't overflow, and the anchor so we get the ellipsis. -const TransactionNameLink = styled(TransactionDetailLink)` +const TransactionNameLink = euiStyled(TransactionDetailLink)` font-family: ${fontFamilyCode}; white-space: nowrap; ${truncate('100%')}; diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index f03c9dd0a2332..414011df7f9ef 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx @@ -7,13 +7,13 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { HeaderMenuPortal } from '../../../../../observability/public'; import { ActionMenu } from '../../../application/action_menu'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { EnvironmentFilter } from '../EnvironmentFilter'; -const HeaderFlexGroup = styled(EuiFlexGroup)` +const HeaderFlexGroup = euiStyled(EuiFlexGroup)` padding: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; `; diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx index 6ab4f2e0388b4..ed91aefdfcf9e 100644 --- a/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx +++ b/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx @@ -7,10 +7,10 @@ import { isBoolean, isNumber, isObject } from 'lodash'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; -const EmptyValue = styled.span` +const EmptyValue = euiStyled.span` color: ${({ theme }) => theme.eui.euiColorMediumShade}; text-align: left; `; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js index fe767f86239b1..46da6fe4be4c9 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js @@ -7,7 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { EuiIcon } from '@elastic/eui'; import { fontFamilyCode, @@ -33,7 +33,7 @@ function getIconColor(type, theme) { } } -const Description = styled.div` +const Description = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorDarkShade}; p { @@ -48,7 +48,7 @@ const Description = styled.div` } `; -const ListItem = styled.li` +const ListItem = euiStyled.li` font-size: ${fontSizes.small}; height: ${px(units.double)}; align-items: center; @@ -68,7 +68,7 @@ const ListItem = styled.li` } `; -const Icon = styled.div` +const Icon = euiStyled.div` flex: 0 0 ${px(units.double)}; background: ${({ type, theme }) => tint(0.1, getIconColor(type, theme))}; color: ${({ type, theme }) => getIconColor(type, theme)}; @@ -78,7 +78,7 @@ const Icon = styled.div` line-height: ${px(units.double)}; `; -const TextValue = styled.div` +const TextValue = euiStyled.div` flex: 0 0 ${px(unit * 16)}; color: ${({ theme }) => theme.eui.euiColorDarkestShade}; padding: 0 ${px(units.half)}; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js index ce0fcab5dea1c..cbbf762fa341c 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js @@ -7,13 +7,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { isEmpty } from 'lodash'; import Suggestion from './Suggestion'; import { units, px, unit } from '../../../../style/variables'; import { tint } from 'polished'; -const List = styled.ul` +const List = euiStyled.ul` width: 100%; border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; border-radius: ${px(units.quarter)}; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 98eb0548b8521..efa4f26d9a23f 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { startsWith, uniqueId } from 'lodash'; import React, { useState } from 'react'; import { useHistory, useLocation, useParams } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { esKuery, IIndexPattern, @@ -24,7 +24,7 @@ import { getBoolFilter } from './get_bool_filter'; import { Typeahead } from './Typeahead'; import { useProcessorEvent } from './use_processor_event'; -const Container = styled.div` +const Container = euiStyled.div` margin-bottom: 10px; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx index 7f8c68ee32ef8..090ba0e8e28cf 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx @@ -6,23 +6,23 @@ */ import React from 'react'; -import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiTitle } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { px, unit, units } from '../../../style/variables'; import { Stacktrace } from '.'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; -const Accordion = styled(EuiAccordion)` +const Accordion = euiStyled(EuiAccordion)` border-top: ${({ theme }) => theme.eui.euiBorderThin}; margin-top: ${px(units.half)}; `; -const CausedByContainer = styled('h5')` +const CausedByContainer = euiStyled('h5')` padding: ${({ theme }) => theme.eui.spacerSizes.s} 0; `; -const CausedByHeading = styled('span')` +const CausedByHeading = euiStyled('span')` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; display: block; font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; @@ -30,7 +30,7 @@ const CausedByHeading = styled('span')` text-transform: uppercase; `; -const FramesContainer = styled('div')` +const FramesContainer = euiStyled('div')` padding-left: ${px(unit)}; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx index 7a503258b2e58..85d29dda95b5c 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx @@ -13,7 +13,7 @@ import python from 'react-syntax-highlighter/dist/cjs/languages/hljs/python'; import ruby from 'react-syntax-highlighter/dist/cjs/languages/hljs/ruby'; import xcode from 'react-syntax-highlighter/dist/cjs/styles/hljs/xcode'; import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { StackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { borderRadius, px, unit, units } from '../../../style/variables'; @@ -21,13 +21,13 @@ SyntaxHighlighter.registerLanguage('javascript', javascript); SyntaxHighlighter.registerLanguage('python', python); SyntaxHighlighter.registerLanguage('ruby', ruby); -const ContextContainer = styled.div` +const ContextContainer = euiStyled.div` position: relative; border-radius: ${borderRadius}; `; const LINE_HEIGHT = units.eighth * 9; -const LineHighlight = styled.div<{ lineNumber: number }>` +const LineHighlight = euiStyled.div<{ lineNumber: number }>` position: absolute; width: 100%; height: ${px(units.eighth * 9)}; @@ -36,7 +36,7 @@ const LineHighlight = styled.div<{ lineNumber: number }>` background-color: ${({ theme }) => tint(0.1, theme.eui.euiColorWarning)}; `; -const LineNumberContainer = styled.div<{ isLibraryFrame: boolean }>` +const LineNumberContainer = euiStyled.div<{ isLibraryFrame: boolean }>` position: absolute; top: 0; left: 0; @@ -47,7 +47,7 @@ const LineNumberContainer = styled.div<{ isLibraryFrame: boolean }>` : theme.eui.euiColorLightestShade}; `; -const LineNumber = styled.div<{ highlight: boolean }>` +const LineNumber = euiStyled.div<{ highlight: boolean }>` position: relative; min-width: ${px(units.eighth * 21)}; padding-left: ${px(units.half)}; @@ -64,7 +64,7 @@ const LineNumber = styled.div<{ highlight: boolean }>` } `; -const LineContainer = styled.div` +const LineContainer = euiStyled.div` overflow: auto; margin: 0 0 0 ${px(units.eighth * 21)}; padding: 0; @@ -75,7 +75,7 @@ const LineContainer = styled.div` } `; -const Line = styled.pre` +const Line = euiStyled.pre` // Override all styles margin: 0; color: inherit; @@ -87,7 +87,7 @@ const Line = styled.pre` line-height: ${px(LINE_HEIGHT)}; `; -const Code = styled.code` +const Code = euiStyled.code` position: relative; padding: 0; margin: 0; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx index 636252b19fe39..68b0893e1d8d3 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx @@ -6,7 +6,7 @@ */ import React, { ComponentType } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { fontFamilyCode, fontSize, px, units } from '../../../style/variables'; import { @@ -18,7 +18,7 @@ import { RubyFrameHeadingRenderer, } from './frame_heading_renderers'; -const FileDetails = styled.div` +const FileDetails = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorDarkShade}; line-height: 1.5; /* matches the line-hight of the accordion container button */ padding: ${px(units.eighth)} 0; @@ -26,12 +26,12 @@ const FileDetails = styled.div` font-size: ${fontSize}; `; -const LibraryFrameFileDetail = styled.span` +const LibraryFrameFileDetail = euiStyled.span` color: ${({ theme }) => theme.eui.euiColorDarkShade}; word-break: break-word; `; -const AppFrameFileDetail = styled.span` +const AppFrameFileDetail = euiStyled.span` color: ${({ theme }) => theme.eui.euiColorFullShade}; word-break: break-word; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx index e67341d68b52f..de417b465638f 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx @@ -8,12 +8,12 @@ import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { px, units } from '../../../style/variables'; import { Stackframe as StackframeComponent } from './Stackframe'; -const LibraryStacktraceAccordion = styled(EuiAccordion)` +const LibraryStacktraceAccordion = euiStyled(EuiAccordion)` margin: ${px(units.quarter)} 0; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx index 4fd90d343146a..d361634759390 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx @@ -7,7 +7,7 @@ import { EuiAccordion } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe as StackframeType, StackframeWithLineContext, @@ -22,7 +22,7 @@ import { FrameHeading } from './FrameHeading'; import { Variables } from './Variables'; import { px, units } from '../../../style/variables'; -const ContextContainer = styled.div<{ isLibraryFrame: boolean }>` +const ContextContainer = euiStyled.div<{ isLibraryFrame: boolean }>` position: relative; font-family: ${fontFamilyCode}; font-size: ${fontSize}; @@ -35,7 +35,7 @@ const ContextContainer = styled.div<{ isLibraryFrame: boolean }>` `; // Indent the non-context frames the same amount as the accordion control -const NoContextFrameHeadingWrapper = styled.div` +const NoContextFrameHeadingWrapper = euiStyled.div` margin-left: ${px(units.unit + units.half + units.quarter)}; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx index 099611d518d55..7c09048593710 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx @@ -5,16 +5,16 @@ * 2.0. */ -import styled from 'styled-components'; import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { borderRadius, px, unit, units } from '../../../style/variables'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { KeyValueTable } from '../KeyValueTable'; import { flattenObject } from '../../../utils/flattenObject'; -const VariablesContainer = styled.div` +const VariablesContainer = euiStyled.div` background: ${({ theme }) => theme.eui.euiColorEmptyShade}; border-radius: 0 0 ${borderRadius} ${borderRadius}; padding: ${px(units.half)} ${px(unit)}; diff --git a/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx b/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx index d07b712e83528..ee764db516d72 100644 --- a/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { fontFamilyCode, fontSizes, @@ -25,11 +25,11 @@ export interface IStickyProperty { truncated?: boolean; } -const TooltipFieldName = styled.span` +const TooltipFieldName = euiStyled.span` font-family: ${fontFamilyCode}; `; -const PropertyLabel = styled.div` +const PropertyLabel = euiStyled.div` margin-bottom: ${px(units.half)}; font-size: ${fontSizes.small}; color: ${({ theme }) => theme.eui.euiColorMediumShade}; @@ -41,13 +41,13 @@ const PropertyLabel = styled.div` PropertyLabel.displayName = 'PropertyLabel'; const propertyValueLineHeight = 1.2; -const PropertyValue = styled.div` +const PropertyValue = euiStyled.div` display: inline-block; line-height: ${propertyValueLineHeight}; `; PropertyValue.displayName = 'PropertyValue'; -const PropertyValueTruncated = styled.span` +const PropertyValueTruncated = euiStyled.span` display: inline-block; line-height: ${propertyValueLineHeight}; ${truncate('100%')}; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index 138afaf256558..ec309f2f74d10 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; import { EuiBadge } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTheme } from '../../../hooks/use_theme'; import { px } from '../../../../public/style/variables'; import { units } from '../../../style/variables'; @@ -17,9 +17,9 @@ interface Props { count: number; } -const Badge = (styled(EuiBadge)` +const Badge = euiStyled(EuiBadge)` margin-top: ${px(units.eighth)}; -` as unknown) as typeof EuiBadge; +`; export function ErrorCountSummaryItemBadge({ count }: Props) { const theme = useTheme(); diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx index 9e8242dfa2a7d..d72f03c386226 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx @@ -8,15 +8,15 @@ import React from 'react'; import { EuiToolTip, EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { units, px, truncate, unit } from '../../../../style/variables'; import { HttpStatusBadge } from '../HttpStatusBadge'; -const HttpInfoBadge = (styled(EuiBadge)` +const HttpInfoBadge = euiStyled(EuiBadge)` margin-right: ${px(units.quarter)}; -` as unknown) as typeof EuiBadge; +`; -const Url = styled('span')` +const Url = euiStyled('span')` display: inline-block; vertical-align: bottom; ${truncate(px(unit * 24))}; @@ -27,7 +27,7 @@ interface HttpInfoProps { url: string; } -const Span = styled('span')` +const Span = euiStyled('span')` white-space: nowrap; `; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx index 703b0787f7923..20fd19a06c9eb 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx @@ -6,14 +6,14 @@ */ import React from 'react'; -import styled from 'styled-components'; import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { UserAgent } from '../../../../typings/es_schemas/raw/fields/user_agent'; type UserAgentSummaryItemProps = UserAgent; -const Version = styled('span')` +const Version = euiStyled('span')` font-size: ${({ theme }) => theme.eui.euiFontSizeS}; `; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx index 357e14ffef356..395156800dceb 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../public/style/variables'; import { Maybe } from '../../../../typings/common'; @@ -15,7 +15,7 @@ interface Props { items: Array>; } -const Item = styled(EuiFlexItem)` +const Item = euiStyled(EuiFlexItem)` flex-wrap: nowrap; border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; padding-right: ${px(units.half)}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx index 8ce60b58c4c44..f81da48b760e7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useTheme } from '../../../../hooks/use_theme'; import { fontSizes, px, units } from '../../../../style/variables'; @@ -22,7 +22,7 @@ interface ContainerProps { disabled: boolean; } -const Container = styled.div` +const Container = euiStyled.div` display: flex; align-items: center; font-size: ${(props) => props.fontSize}; @@ -39,7 +39,7 @@ interface IndicatorProps { withMargin: boolean; } -export const Indicator = styled.span` +export const Indicator = euiStyled.span` width: ${(props) => px(props.radius)}; height: ${(props) => px(props.radius)}; margin-right: ${(props) => (props.withMargin ? px(props.radius / 2) : 0)}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx index ad8b85ba70c9b..3b7f0fab6c2a7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx @@ -7,19 +7,19 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../common/utils/formatters'; import { useTheme } from '../../../../../hooks/use_theme'; import { px, units } from '../../../../../style/variables'; import { Legend } from '../../Legend'; import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; -const NameContainer = styled.div` +const NameContainer = euiStyled.div` border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; padding-bottom: ${px(units.half)}; `; -const TimeContainer = styled.div` +const TimeContainer = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorMediumShade}; padding-top: ${px(units.half)}; `; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx index 393281b2bf848..044070303d2ff 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { EuiPopover, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../common/utils/formatters'; import { useTheme } from '../../../../../hooks/use_theme'; import { @@ -24,21 +24,21 @@ interface Props { mark: ErrorMark; } -const Popover = styled.div` +const Popover = euiStyled.div` max-width: ${px(280)}; `; -const TimeLegend = styled(Legend)` +const TimeLegend = euiStyled(Legend)` margin-bottom: ${px(unit)}; `; -const ErrorLink = styled(ErrorDetailLink)` +const ErrorLink = euiStyled(ErrorDetailLink)` display: block; margin: ${px(units.half)} 0 ${px(units.half)} 0; overflow-wrap: break-word; `; -const Button = styled(Legend)` +const Button = euiStyled(Legend)` height: 20px; display: flex; align-items: flex-end; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx index b426a10a7562d..bece72b398d31 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { px } from '../../../../../style/variables'; import { AgentMarker } from './AgentMarker'; import { ErrorMarker } from './ErrorMarker'; @@ -18,7 +18,7 @@ interface Props { x: number; } -const MarkerContainer = styled.div` +const MarkerContainer = euiStyled.div` position: absolute; bottom: 0; `; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index cbadbb0cf4f81..a64355e47f757 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React from 'react'; import { useParams } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { MLSingleMetricLink } from '../../Links/MachineLearningLinks/MLSingleMetricLink'; @@ -20,14 +20,14 @@ interface Props { mlJobId?: string; } -const ShiftedIconWrapper = styled.span` +const ShiftedIconWrapper = euiStyled.span` padding-right: 5px; position: relative; top: -1px; display: inline-block; `; -const ShiftedEuiText = styled(EuiText)` +const ShiftedEuiText = euiStyled(EuiText)` position: relative; top: 5px; `; diff --git a/x-pack/plugins/apm/public/components/shared/main_tabs.tsx b/x-pack/plugins/apm/public/components/shared/main_tabs.tsx index de4b368efdbbc..941ce924cff07 100644 --- a/x-pack/plugins/apm/public/components/shared/main_tabs.tsx +++ b/x-pack/plugins/apm/public/components/shared/main_tabs.tsx @@ -7,12 +7,12 @@ import { EuiTabs } from '@elastic/eui'; import React, { ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; // Since our `EuiTab` components have `APMLink`s inside of them and not just // `href`s, we need to override the color of the links inside or they will all // be the primary color. -const StyledTabs = styled(EuiTabs)` +const StyledTabs = euiStyled(EuiTabs)` padding: ${({ theme }) => `${theme.eui.gutterTypes.gutterMedium}`}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; `; diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 34ba1d86264c1..3285db1f49191 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -7,14 +7,14 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { px, unit } from '../../style/variables'; import { DatePicker } from './DatePicker'; import { KueryBar } from './KueryBar'; import { TimeComparison } from './time_comparison'; import { useBreakPoints } from '../../hooks/use_break_points'; -const SearchBarFlexGroup = styled(EuiFlexGroup)` +const SearchBarFlexGroup = euiStyled(EuiFlexGroup)` margin: ${({ theme }) => `${theme.eui.euiSizeS} ${theme.eui.euiSizeS} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeS}`}; `; diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index 02064ea786fb0..e4b03bd57377a 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -10,14 +10,14 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { getDateDifference } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { px, unit } from '../../../style/variables'; import * as urlHelpers from '../../shared/Links/url_helpers'; import { useBreakPoints } from '../../../hooks/use_break_points'; -const PrependContainer = styled.div` +const PrependContainer = euiStyled.div` display: flex; justify-content: center; align-items: center; diff --git a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx index c6e939de2b064..63e0b84362073 100644 --- a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx @@ -7,12 +7,12 @@ import { EuiToolTip } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { truncate } from '../../../style/variables'; const tooltipAnchorClassname = '_apm_truncate_tooltip_anchor_'; -const TooltipWrapper = styled.div` +const TooltipWrapper = euiStyled.div` width: 100%; .${tooltipAnchorClassname} { width: 100% !important; @@ -20,7 +20,7 @@ const TooltipWrapper = styled.div` } `; -const ContentWrapper = styled.div` +const ContentWrapper = euiStyled.div` ${truncate('100%')} `; From a62a229d6f719eea698114676638d2e6c4339757 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Tue, 9 Feb 2021 03:05:38 -0500 Subject: [PATCH 039/100] [Grok Debugger] Changed test to wait for grok debugger container to exist to fix test flakiness (#90543) * Changed the retry to a wait for condition which will keep trying for 20 seconds to see the container. Also changed the data test subject as there was another grokDebugger test subject just in case there was a clash. * Added .only to be able to run the test repeatedly because group 13 is not in the flaky test runner. * Added to group 11 because 13 is not in flaky test runner. Will revert after passing. * Reverted change back to group 13 and removed the comment for the flaky test being skipped. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/grok_debugger/grok_debugger.js | 2 +- .../test/functional/apps/grok_debugger/grok_debugger.js | 3 +-- x-pack/test/functional/services/grok_debugger.js | 8 +++----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js b/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js index aa566b0562802..17a6408298b07 100644 --- a/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js +++ b/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js @@ -129,7 +129,7 @@ export class GrokDebuggerComponent extends React.Component { - + diff --git a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js index 010341cedd3a7..b2a1c5363fcb6 100644 --- a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js +++ b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js @@ -12,8 +12,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['grokDebugger']); - // FLAKY: https://github.com/elastic/kibana/issues/84440 - describe.skip('grok debugger app', function () { + describe('grok debugger app', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('empty_kibana'); diff --git a/x-pack/test/functional/services/grok_debugger.js b/x-pack/test/functional/services/grok_debugger.js index 730b4ca60c05a..42a80edd70c85 100644 --- a/x-pack/test/functional/services/grok_debugger.js +++ b/x-pack/test/functional/services/grok_debugger.js @@ -13,7 +13,7 @@ export function GrokDebuggerProvider({ getService }) { const retry = getService('retry'); // test subject selectors - const SUBJ_CONTAINER = 'grokDebugger'; + const SUBJ_CONTAINER = 'grokDebuggerContainer'; const SUBJ_UI_ACE_EVENT_INPUT = `${SUBJ_CONTAINER} > aceEventInput > codeEditorContainer`; const SUBJ_UI_ACE_PATTERN_INPUT = `${SUBJ_CONTAINER} > acePatternInput > codeEditorContainer`; @@ -49,10 +49,8 @@ export function GrokDebuggerProvider({ getService }) { } async assertExists() { - await retry.try(async () => { - if (!(await testSubjects.exists(SUBJ_CONTAINER))) { - throw new Error('Expected to find the grok debugger'); - } + await retry.waitFor('Grok Debugger to exist', async () => { + return await testSubjects.exists(SUBJ_CONTAINER); }); } From cbe5f0d9ca459c571759eb339c2ea62fad600355 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Tue, 9 Feb 2021 11:30:54 +0300 Subject: [PATCH 040/100] Add folding in kb-monaco and update some viewers (#90152) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-monaco/src/monaco_imports.ts | 2 +- .../views/requests/components/details/req_code_viewer.tsx | 4 +++- .../public/vega_inspector/components/spec_viewer.tsx | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/kbn-monaco/src/monaco_imports.ts b/packages/kbn-monaco/src/monaco_imports.ts index 19fccd3f03934..872ac46352cf3 100644 --- a/packages/kbn-monaco/src/monaco_imports.ts +++ b/packages/kbn-monaco/src/monaco_imports.ts @@ -17,7 +17,7 @@ import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands.js'; import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget.js'; import 'monaco-editor/esm/vs/editor/contrib/wordOperations/wordOperations.js'; // Needed for word-wise char navigation - +import 'monaco-editor/esm/vs/editor/contrib/folding/folding.js'; // Needed for folding import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController.js'; // Needed for suggestions import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature diff --git a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx index 39a13b9f9afcf..30aa32c6a2e89 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { XJsonLang } from '@kbn/monaco'; import { EuiFlexItem, EuiFlexGroup, EuiCopy, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { CodeEditor } from '../../../../../../kibana_react/public'; @@ -51,7 +52,7 @@ export const RequestCodeViewer = ({ json }: RequestCodeViewerProps) => ( {}} options={{ @@ -61,6 +62,7 @@ export const RequestCodeViewer = ({ json }: RequestCodeViewerProps) => ( minimap: { enabled: false, }, + folding: true, scrollBeyondLastLine: false, wordWrap: 'on', wrappingIndent: 'indent', diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx index b25024b3c8d3a..178854550aff1 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; +import { XJsonLang } from '@kbn/monaco'; import { EuiFlexItem, @@ -71,7 +72,7 @@ export const SpecViewer = ({ vegaAdapter, ...rest }: SpecViewerProps) => { {}} options={{ @@ -82,6 +83,7 @@ export const SpecViewer = ({ vegaAdapter, ...rest }: SpecViewerProps) => { enabled: false, }, scrollBeyondLastLine: false, + folding: true, wordWrap: 'on', wrappingIndent: 'indent', automaticLayout: true, From fe6dae987e885df3b49aca0de439da40075b8122 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Tue, 9 Feb 2021 08:45:19 +0000 Subject: [PATCH 041/100] [APM-UI][E2E] use withGithubStatus step (#90651) --- .ci/end2end.groovy | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index a89ff166bf32e..87b64437deafc 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -121,15 +121,9 @@ pipeline { } def notifyStatus(String description, String status) { - notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('pipeline')) + withGithubStatus.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('pipeline')) } def notifyTestStatus(String description, String status) { - notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('tests')) -} - -def notify(Map args = [:]) { - retryWithSleep(retries: 2, seconds: 5, backoff: true) { - githubNotify(context: args.context, description: args.description, status: args.status, targetUrl: args.targetUrl) - } + withGithubStatus.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('tests')) } From f94aacecd9843d62fe42ade16d39c9b9ca1c7dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Tue, 9 Feb 2021 09:50:37 +0100 Subject: [PATCH 042/100] [ILM] Delete phase redesign (rework) (#90291) * Phases redesign * Fixed scss file * Fixed errors * Added changes for phase blocks * Added styles adjustments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../edit_policy/edit_policy.helpers.tsx | 23 ++- .../edit_policy/edit_policy.test.ts | 10 +- .../__jest__/components/edit_policy.test.tsx | 37 +++- .../edit_policy/components/active_badge.tsx | 18 -- .../active_highlight/active_highlight.scss | 16 -- .../sections/edit_policy/components/index.ts | 4 +- .../index.ts | 2 +- .../infinity_icon.svg.tsx | 0 .../infinity_icon.tsx} | 14 +- .../components/phase_footer/index.ts | 8 + .../components/phase_footer/phase_footer.tsx | 94 ++++++++++ .../components/phase_icon/index.ts | 8 + .../components/phase_icon/phase_icon.scss | 33 ++++ .../components/phase_icon/phase_icon.tsx | 32 ++++ .../phases/delete_phase/delete_phase.scss | 11 ++ .../phases/delete_phase/delete_phase.tsx | 142 ++++++-------- .../components/phases/phase/phase.scss | 27 +++ .../components/phases/phase/phase.tsx | 174 +++++++++--------- .../phases/phase/phase_error_indicator.tsx | 3 +- .../phases/shared_fields/forcemerge_field.tsx | 37 ++-- .../min_age_field/min_age_field.tsx | 7 +- .../searchable_snapshot_field.tsx | 1 - .../shared_fields/snapshot_policies_field.tsx | 119 ++++++++---- .../components/timeline/timeline.tsx | 19 +- .../sections/edit_policy/edit_policy.tsx | 16 +- .../edit_policy/form/components/form.tsx | 5 +- .../sections/edit_policy/form/index.ts | 6 + .../form/phase_timings_context.tsx | 73 ++++++++ .../sections/edit_policy/form/schema.ts | 14 +- .../sections/edit_policy/i18n_texts.ts | 10 + ...absolute_timing_to_relative_timing.test.ts | 18 +- .../lib/absolute_timing_to_relative_timing.ts | 6 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 34 files changed, 672 insertions(+), 321 deletions(-) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{active_highlight => infinity_icon}/index.ts (82%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{timeline => infinity_icon}/infinity_icon.svg.tsx (100%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{active_highlight/active_highlight.tsx => infinity_icon/infinity_icon.tsx} (51%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.scss create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.scss create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index a59c4d9878aea..dc375f6370048 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -248,6 +248,27 @@ export const setup = async (arg?: { appServicesContext: Partial { + const enablePhase = async () => { + await act(async () => { + find('enableDeletePhaseButton').simulate('click'); + }); + component.update(); + }; + + const disablePhase = async () => { + await act(async () => { + find('disableDeletePhaseButton').simulate('click'); + }); + component.update(); + }; + + return { + enablePhase, + disablePhase, + }; + }; + return { ...testBed, actions: { @@ -303,7 +324,7 @@ export const setup = async (arg?: { appServicesContext: Partial', () => { // Set max docs to test whether we keep the unknown fields in that object after serializing await actions.hot.setMaxDocs('1000'); // Remove the delete phase to ensure that we also correctly remove data - await actions.delete.enable(false); + await actions.delete.disablePhase(); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; @@ -89,7 +89,7 @@ describe('', () => { unknown_setting: true, }, }, - min_age: '0ms', + min_age: '0d', }, }, }); @@ -255,7 +255,7 @@ describe('', () => { "priority": 50, }, }, - "min_age": "0ms", + "min_age": "0d", } `); }); @@ -310,7 +310,7 @@ describe('', () => { "number_of_shards": 123, }, }, - "min_age": "0ms", + "min_age": "0d", }, }, } @@ -839,7 +839,7 @@ describe('', () => { expect(actions.timeline.hasColdPhase()).toBe(true); expect(actions.timeline.hasDeletePhase()).toBe(false); - await actions.delete.enable(true); + await actions.delete.enablePhase(); expect(actions.timeline.hasHotPhase()).toBe(true); expect(actions.timeline.hasWarmPhase()).toBe(true); expect(actions.timeline.hasColdPhase()).toBe(true); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index a9a351e394f7f..7c199e2ced765 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -99,6 +99,13 @@ const activatePhase = async (rendered: ReactWrapper, phase: string) => { }); rendered.update(); }; +const activateDeletePhase = async (rendered: ReactWrapper) => { + const testSubject = `enableDeletePhaseButton`; + await act(async () => { + await findTestSubject(rendered, testSubject).simulate('click'); + }); + rendered.update(); +}; const openNodeAttributesSection = async (rendered: ReactWrapper, phase: string) => { const getControls = () => findTestSubject(rendered, `${phase}-dataTierAllocationControls`); await act(async () => { @@ -454,6 +461,11 @@ describe('edit policy', () => { waitForFormLibValidation(rendered); expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); + + test("doesn't show min age input", async () => { + const rendered = mountWithIntl(component); + expect(findTestSubject(rendered, 'hot-selectedMinimumAge').exists()).toBeFalsy(); + }); }); describe('warm phase', () => { beforeEach(() => { @@ -670,6 +682,13 @@ describe('edit policy', () => { expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); }); + + test('shows min age input only when enabled', async () => { + const rendered = mountWithIntl(component); + expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeFalsy(); + await activatePhase(rendered, 'warm'); + expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeTruthy(); + }); }); describe('cold phase', () => { beforeEach(() => { @@ -807,13 +826,20 @@ describe('edit policy', () => { expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); }); + + test('shows min age input only when enabled', async () => { + const rendered = mountWithIntl(component); + expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeFalsy(); + await activatePhase(rendered, 'cold'); + expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeTruthy(); + }); }); describe('delete phase', () => { test('should allow 0 for phase timing', async () => { const rendered = mountWithIntl(component); await noRollover(rendered); await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'delete'); + await activateDeletePhase(rendered); await setPhaseAfter(rendered, 'delete', '0'); waitForFormLibValidation(rendered); expectedErrorMessages(rendered, []); @@ -822,11 +848,18 @@ describe('edit policy', () => { const rendered = mountWithIntl(component); await noRollover(rendered); await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'delete'); + await activateDeletePhase(rendered); await setPhaseAfter(rendered, 'delete', '-1'); waitForFormLibValidation(rendered); expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); + + test('is hidden when disabled', async () => { + const rendered = mountWithIntl(component); + expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeFalsy(); + await activateDeletePhase(rendered); + expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeTruthy(); + }); }); describe('not on cloud', () => { beforeEach(() => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx deleted file mode 100644 index f3a6ee7276cde..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx +++ /dev/null @@ -1,18 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiBadge } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const ActiveBadge = () => { - return ( - - - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss deleted file mode 100644 index 96ca0c3a61067..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss +++ /dev/null @@ -1,16 +0,0 @@ -.ilmActivePhaseHighlight { - border-left: $euiBorderWidthThin solid $euiColorLightShade; - height: 100%; - - &.hotPhase.active { - border-left-color: $euiColorVis9_behindText; - } - - &.warmPhase.active { - border-left-color: $euiColorVis5_behindText; - } - - &.coldPhase.active { - border-left-color: $euiColorVis1_behindText; - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index a84d15e6c19da..dc4f1e31d3696 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -5,14 +5,12 @@ * 2.0. */ -export { ActiveBadge } from './active_badge'; export { LearnMoreLink } from './learn_more_link'; export { OptionalLabel } from './optional_label'; export { PolicyJsonFlyout } from './policy_json_flyout'; export { DescribedFormRow, ToggleFieldWithDescribedFormRow } from './described_form_row'; export { FieldLoadingError } from './field_loading_error'; -export { ActiveHighlight } from './active_highlight'; export { Timeline } from './timeline'; export { FormErrorsCallout } from './form_errors_callout'; - +export { PhaseFooter } from './phase_footer'; export * from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/index.ts similarity index 82% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/index.ts index c8d3b6540dc3d..850f3e4e07aed 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { ActiveHighlight } from './active_highlight'; +export { InfinityIcon } from './infinity_icon'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/infinity_icon.svg.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/infinity_icon.svg.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/infinity_icon.svg.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/infinity_icon.svg.tsx diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/infinity_icon.tsx similarity index 51% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/infinity_icon.tsx index bae73c3cefa5d..435e6a909acd1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/infinity_icon.tsx @@ -6,13 +6,9 @@ */ import React, { FunctionComponent } from 'react'; +import { EuiIcon, EuiIconProps } from '@elastic/eui'; +import { InfinityIconSvg } from './infinity_icon.svg'; -import './active_highlight.scss'; - -interface Props { - phase: 'hot' | 'warm' | 'cold'; - enabled: boolean; -} -export const ActiveHighlight: FunctionComponent = ({ phase, enabled }) => { - return

; -}; +export const InfinityIcon: FunctionComponent> = (props) => ( + +); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/index.ts new file mode 100644 index 0000000000000..724904a1f188e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { PhaseFooter } from './phase_footer'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx new file mode 100644 index 0000000000000..82f0725bfe7d0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiText, EuiButtonGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { PhasesExceptDelete } from '../../../../../../common/types'; + +import { usePhaseTimings } from '../../form'; + +import { InfinityIconSvg } from '../infinity_icon/infinity_icon.svg'; + +interface Props { + phase: PhasesExceptDelete; +} + +export const PhaseFooter: FunctionComponent = ({ phase }) => { + const { + isDeletePhaseEnabled, + setDeletePhaseEnabled: setValue, + [phase]: phaseConfiguration, + } = usePhaseTimings(); + + if (!phaseConfiguration.isFinalDataPhase) { + return null; + } + + const phaseDescription = isDeletePhaseEnabled + ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseTiming.beforeDeleteDescription', { + defaultMessage: 'Data will be deleted after this phase', + }) + : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseTiming.foreverTimingDescription', { + defaultMessage: 'Data will remain in this phase forever', + }); + + const selectedButton = isDeletePhaseEnabled + ? 'ilmEnableDeletePhaseButton' + : 'ilmDisableDeletePhaseButton'; + + const buttons = [ + { + id: `ilmDisableDeletePhaseButton`, + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.disablePhaseButtonLabel', + { + defaultMessage: 'Keep data in this phase forever', + } + ), + iconType: InfinityIconSvg, + }, + { + id: `ilmEnableDeletePhaseButton`, + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.enablePhaseButtonLabel', + { + defaultMessage: 'Delete data after this phase', + } + ), + iconType: 'trash', + 'data-test-subj': 'enableDeletePhaseButton', + }, + ]; + + return ( + + + + {phaseDescription} + + + + { + setValue(id === 'ilmEnableDeletePhaseButton'); + }} + isIconOnly={true} + /> + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/index.ts new file mode 100644 index 0000000000000..26fda5d929284 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { PhaseIcon } from './phase_icon'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss new file mode 100644 index 0000000000000..7c6a5aefdde6e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss @@ -0,0 +1,33 @@ +.ilmPhaseIcon { + width: $euiSizeXL; + height: $euiSizeXL; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + background-color: $euiColorLightestShade; + &--disabled { + margin-top: $euiSizeS; + width: $euiSize; + height: $euiSize; + } + &--delete { + background-color: $euiColorLightShade; + } + &__inner--hot { + fill: $euiColorVis9_behindText; + } + &__inner--warm { + fill: $euiColorVis5_behindText; + } + &__inner--cold { + fill: $euiColorVis1_behindText; + } + &__inner--delete { + fill: $euiColorDarkShade; + } + + &__inner--disabled { + fill: $euiColorMediumShade; + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.tsx new file mode 100644 index 0000000000000..8c0a0bcca1d76 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiIcon } from '@elastic/eui'; +import { Phases } from '../../../../../../common/types'; +import './phase_icon.scss'; +interface Props { + enabled: boolean; + phase: string & keyof Phases; +} +export const PhaseIcon: FunctionComponent = ({ enabled, phase }) => { + return ( +
+ {enabled ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.scss new file mode 100644 index 0000000000000..60a39c7f1e9a6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.scss @@ -0,0 +1,11 @@ +.ilmDeletePhase { + .euiCommentEvent { + &__header { + padding: $euiSize; + background-color: $euiColorEmptyShade; + } + &__body { + padding: $euiSize; + } + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx index c2da9246effb7..c65699ca12690 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx @@ -5,107 +5,85 @@ * 2.0. */ -import React, { FunctionComponent, Fragment } from 'react'; +import React, { FunctionComponent } from 'react'; import { get } from 'lodash'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiTitle, + EuiButtonEmpty, + EuiSpacer, + EuiText, + EuiComment, +} from '@elastic/eui'; + import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiDescribedFormGroup, EuiTextColor, EuiFormRow } from '@elastic/eui'; -import { useFormData, ToggleField } from '../../../../../../shared_imports'; +import { useFormData } from '../../../../../../shared_imports'; -import { UseField } from '../../../form'; +import { i18nTexts } from '../../../i18n_texts'; -import { ActiveBadge, LearnMoreLink, OptionalLabel } from '../../index'; +import { usePhaseTimings } from '../../../form'; import { MinAgeField, SnapshotPoliciesField } from '../shared_fields'; +import './delete_phase.scss'; +import { PhaseIcon } from '../../phase_icon'; +import { PhaseErrorIndicator } from '../phase/phase_error_indicator'; const formFieldPaths = { enabled: '_meta.delete.enabled', }; export const DeletePhase: FunctionComponent = () => { + const { setDeletePhaseEnabled } = usePhaseTimings(); const [formData] = useFormData({ watch: formFieldPaths.enabled, }); const enabled = get(formData, formFieldPaths.enabled); - return ( -
- -

- -

{' '} - {enabled && } -
- } - titleSize="s" - description={ - -

- -

- -
- } - fullWidth - > - {enabled && } - - {enabled ? ( - - - - } - description={ - - {' '} - - - } - titleSize="xs" - fullWidth + if (!enabled) { + return null; + } + const phaseTitle = ( + + + +

{i18nTexts.editPolicy.titles.delete}

+
+
+ + + setDeletePhaseEnabled(false)} + data-test-subj={'disableDeletePhaseButton'} > - - - - - } - > - - -
- ) : null} -
+ + +
+ + + + +
+ ); + + return ( + } + className="ilmDeletePhase ilmPhase" + timelineIcon={} + > + + {i18nTexts.editPolicy.descriptions.delete} + + + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.scss new file mode 100644 index 0000000000000..15f2dc508a365 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.scss @@ -0,0 +1,27 @@ +.ilmPhase { + .euiCommentEvent { + &__header { + padding: $euiSize; + } + &__body { + padding: $euiSize; + } + } + .ilmSettingsButton { + color: $euiColorPrimary; + padding: $euiSizeS; + } + .euiCommentTimeline { + padding-top: $euiSize; + &::before { + height: calc(100% + #{$euiSizeXXL}); + } + } + &--enabled { + .euiCommentEvent { + &__header { + background-color: $euiColorEmptyShade; + } + } + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx index f7e0f8e20e050..0ac6f6922ec1e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx @@ -5,126 +5,120 @@ * 2.0. */ -import React, { FunctionComponent, useState } from 'react'; +import React, { FunctionComponent } from 'react'; import { EuiFlexGroup, EuiFlexItem, - EuiPanel, EuiTitle, - EuiSpacer, EuiText, - EuiButtonEmpty, + EuiComment, + EuiAccordion, + EuiSpacer, + EuiBadge, } from '@elastic/eui'; import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; +import { PhasesExceptDelete } from '../../../../../../../common/types'; import { ToggleField, useFormData } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; +import { FormInternal } from '../../../types'; + import { UseField } from '../../../form'; -import { ActiveHighlight } from '../../active_highlight'; -import { MinAgeField } from '../shared_fields'; import { PhaseErrorIndicator } from './phase_error_indicator'; +import { MinAgeField } from '../shared_fields'; +import { PhaseIcon } from '../../phase_icon'; +import { PhaseFooter } from '../../phase_footer'; +import './phase.scss'; + interface Props { - phase: 'hot' | 'warm' | 'cold'; + phase: PhasesExceptDelete; } export const Phase: FunctionComponent = ({ children, phase }) => { const enabledPath = `_meta.${phase}.enabled`; - const [formData] = useFormData({ + const [formData] = useFormData({ watch: [enabledPath], }); + const isHotPhase = phase === 'hot'; // hot phase is always enabled - const enabled = get(formData, enabledPath) || phase === 'hot'; + const enabled = get(formData, enabledPath) || isHotPhase; - const [isShowingSettings, setShowingSettings] = useState(false); - return ( - + const phaseTitle = ( + + {!isHotPhase && ( + + + + )} - + +

{i18nTexts.editPolicy.titles[phase]}

+
- - - - - - {phase !== 'hot' && ( - - - - )} - - - - -

{i18nTexts.editPolicy.titles[phase]}

-
-
- - - -
-
-
-
- {enabled && ( - - - - {phase !== 'hot' && } - - - { - setShowingSettings(!isShowingSettings); - }} - size="xs" - iconType="controlsVertical" - iconSide="left" - aria-controls={`${phase}-phaseContent`} - > - - - - - - )} -
- - - {i18nTexts.editPolicy.descriptions[phase]} - - - {enabled && ( -
- - {children} -
- )} -
+ {isHotPhase && ( + + + + + + )} + +
); + + // @ts-ignore + const minAge = !isHotPhase && enabled ? : null; + + return ( + } + className={`ilmPhase ${enabled ? 'ilmPhase--enabled' : ''}`} + > + + {i18nTexts.editPolicy.descriptions[phase]} + + + {enabled && ( + <> + + + } + buttonClassName="ilmSettingsButton" + extraAction={} + > + + {children} + + + )} + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx index 98fdfe73ecbd8..647f12669cf77 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx @@ -9,10 +9,11 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent, memo } from 'react'; import { EuiIconTip } from '@elastic/eui'; +import { Phases } from '../../../../../../../common/types'; import { useFormErrorsContext } from '../../../form'; interface Props { - phase: 'hot' | 'warm' | 'cold'; + phase: string & keyof Phases; } const i18nTexts = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx index 2d5f5babe1e2a..bbdcbbf4759ef 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx @@ -8,7 +8,9 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CheckBoxField, NumericField } from '../../../../../../shared_imports'; +import uuid from 'uuid'; +import { EuiCheckbox, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; +import { NumericField } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; @@ -67,16 +69,29 @@ export const ForcemergeField: React.FunctionComponent = ({ phase }) => { }, }} /> - + + + {(field) => ( + + + + + + + + + + )} + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx index 60af830356ab9..2f1a058f5a943 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx @@ -75,7 +75,12 @@ export const MinAgeField: FunctionComponent = ({ phase }): React.ReactEle const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( - + = ({ phase }) => config={{ defaultValue: cloud?.isCloudEnabled ? CLOUD_DEFAULT_REPO : undefined, - label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel, validations: [ { validator: emptyField( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx index 2cbd5cea6165a..f9c973d14b3e2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx @@ -10,7 +10,13 @@ import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiCallOut, EuiComboBoxOptionOption, EuiLink, EuiSpacer } from '@elastic/eui'; +import { + EuiCallOut, + EuiComboBoxOptionOption, + EuiDescribedFormGroup, + EuiLink, + EuiSpacer, +} from '@elastic/eui'; import { ComboBoxField, useFormData } from '../../../../../../shared_imports'; import { useLoadSnapshotPolicies } from '../../../../../services/api'; @@ -18,7 +24,7 @@ import { useLoadSnapshotPolicies } from '../../../../../services/api'; import { useEditPolicyContext } from '../../../edit_policy_context'; import { UseField } from '../../../form'; -import { FieldLoadingError } from '../../'; +import { FieldLoadingError, LearnMoreLink, OptionalLabel } from '../../'; const waitForSnapshotFormField = 'phases.delete.actions.wait_for_snapshot.policy'; @@ -137,43 +143,78 @@ export const SnapshotPoliciesField: React.FunctionComponent = () => { } return ( - <> - path={waitForSnapshotFormField}> - {(field) => { - const singleSelectionArray: [selectedSnapshot?: string] = field.value - ? [field.value] - : []; + + + + } + description={ + <> + {' '} + + + } + titleSize="xs" + fullWidth + > + <> + + path={waitForSnapshotFormField} + componentProps={{ + label: ( + <> + + + + ), + }} + > + {(field) => { + const singleSelectionArray: [selectedSnapshot?: string] = field.value + ? [field.value] + : []; - return ( - { - field.setValue(newOption); - }, - onChange: (options: EuiComboBoxOptionOption[]) => { - if (options.length > 0) { - field.setValue(options[0].label); - } else { - field.setValue(''); - } - }, - }} - /> - ); - }} - - {calloutContent} - + return ( + { + field.setValue(newOption); + }, + onChange: (options: EuiComboBoxOptionOption[]) => { + if (options.length > 0) { + field.setValue(options[0].label); + } else { + field.setValue(''); + } + }, + }} + /> + ); + }} + + {calloutContent} + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx index 3ebd5935b8d3f..2d83009bd4df4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx @@ -6,15 +6,10 @@ */ import { i18n } from '@kbn/i18n'; + import React, { FunctionComponent, memo } from 'react'; -import { - EuiIcon, - EuiIconProps, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiIconTip, -} from '@elastic/eui'; + +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiIconTip } from '@elastic/eui'; import { PhasesExceptDelete } from '../../../../../../common/types'; @@ -25,15 +20,13 @@ import { AbsoluteTimings, } from '../../lib'; -import './timeline.scss'; -import { InfinityIconSvg } from './infinity_icon.svg'; +import { InfinityIcon } from '../infinity_icon'; + import { TimelinePhaseText } from './components'; const exists = (v: unknown) => v != null; -const InfinityIcon: FunctionComponent> = (props) => ( - -); +import './timeline.scss'; const toPercent = (n: number, total: number) => (n / total) * 100; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index 749327a2dd441..0c7b5565372a5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -239,19 +239,19 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { - +
+ - + - + - + - + - - - + +
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx index 429ae37b76013..be8243cab289f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx @@ -11,6 +11,7 @@ import { Form as LibForm, FormHook } from '../../../../../shared_imports'; import { ConfigurationIssuesProvider } from '../configuration_issues_context'; import { FormErrorsProvider } from '../form_errors_context'; +import { PhaseTimingsProvider } from '../phase_timings_context'; interface Props { form: FormHook; @@ -19,7 +20,9 @@ interface Props { export const Form: FunctionComponent = ({ form, children }) => ( - {children} + + {children} + ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts index 753148f55db42..734a12a72bd30 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts @@ -21,3 +21,9 @@ export { } from './configuration_issues_context'; export { FormErrorsProvider, useFormErrorsContext } from './form_errors_context'; + +export { + PhaseTimingsProvider, + usePhaseTimings, + PhaseTimingConfiguration, +} from './phase_timings_context'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx new file mode 100644 index 0000000000000..92cc8eeead91a --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, FunctionComponent, useContext } from 'react'; +import { useFormData } from '../../../../shared_imports'; +import { FormInternal } from '../types'; +import { UseField } from './index'; + +export interface PhaseTimingConfiguration { + /** + * Whether this is the final, non-delete, phase. + */ + isFinalDataPhase: boolean; +} + +const getPhaseTimingConfiguration = ( + formData: FormInternal +): { + hot: PhaseTimingConfiguration; + warm: PhaseTimingConfiguration; + cold: PhaseTimingConfiguration; +} => { + const isWarmPhaseEnabled = formData?._meta?.warm?.enabled; + const isColdPhaseEnabled = formData?._meta?.cold?.enabled; + return { + hot: { isFinalDataPhase: !isWarmPhaseEnabled && !isColdPhaseEnabled }, + warm: { isFinalDataPhase: isWarmPhaseEnabled && !isColdPhaseEnabled }, + cold: { isFinalDataPhase: isColdPhaseEnabled }, + }; +}; +export interface PhaseTimings { + hot: PhaseTimingConfiguration; + warm: PhaseTimingConfiguration; + cold: PhaseTimingConfiguration; + isDeletePhaseEnabled: boolean; + setDeletePhaseEnabled: (enabled: boolean) => void; +} + +const PhaseTimingsContext = createContext(null as any); + +export const PhaseTimingsProvider: FunctionComponent = ({ children }) => { + const [formData] = useFormData({ + watch: ['_meta.warm.enabled', '_meta.cold.enabled', '_meta.delete.enabled'], + }); + + return ( + + {(field) => { + return ( + + {children} + + ); + }} + + ); +}; +export const usePhaseTimings = () => { + const ctx = useContext(PhaseTimingsContext); + if (!ctx) throw new Error('Cannot use phase timings outside of phase timings context'); + + return ctx; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index ee84be231f4cc..600a660657863 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -70,7 +70,7 @@ export const schema: FormSchema = { ), }, minAgeUnit: { - defaultValue: 'ms', + defaultValue: 'd', }, bestCompression: { label: i18nTexts.editPolicy.bestCompressionFieldLabel, @@ -361,6 +361,18 @@ export const schema: FormSchema = { }, ], }, + actions: { + wait_for_snapshot: { + policy: { + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.waitForSnapshot.snapshotPolicyFieldLabel', + { + defaultMessage: 'Policy name (optional)', + } + ), + }, + }, + }, }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 55af738d7d7ae..5deba8607cd52 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -188,6 +188,9 @@ export const i18nTexts = { cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle', { defaultMessage: 'Cold phase', }), + delete: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseTitle', { + defaultMessage: 'Delete Data', + }), }, descriptions: { hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescription', { @@ -202,6 +205,13 @@ export const i18nTexts = { defaultMessage: 'You are querying your index less frequently, so you can allocate shards on significantly less performant hardware. Because your queries are slower, you can reduce the number of replicas.', }), + delete: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescription', + { + defaultMessage: + 'You no longer need your index. You can define when it is safe to delete it.', + } + ), }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts index 9f96bbfb25c72..7ec20cc2a5966 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts @@ -289,7 +289,7 @@ describe('Conversion of absolute policy timing to relative timing', () => { }, }) ) - ).toEqual({ total: 'Forever', hot: 'Forever', warm: undefined, cold: undefined }); + ).toEqual({ total: 'forever', hot: 'forever', warm: undefined, cold: undefined }); }); test('hot, then always warm', () => { @@ -308,7 +308,7 @@ describe('Conversion of absolute policy timing to relative timing', () => { }, }) ) - ).toEqual({ total: 'Forever', hot: 'Less than a day', warm: 'Forever', cold: undefined }); + ).toEqual({ total: 'forever', hot: 'less than a day', warm: 'forever', cold: undefined }); }); test('hot, then warm, then always cold', () => { @@ -333,10 +333,10 @@ describe('Conversion of absolute policy timing to relative timing', () => { }) ) ).toEqual({ - total: 'Forever', + total: 'forever', hot: '30 days', warm: '4 days', - cold: 'Forever', + cold: 'forever', }); }); @@ -357,7 +357,7 @@ describe('Conversion of absolute policy timing to relative timing', () => { }, }) ) - ).toEqual({ total: 'Forever', hot: '34 days', warm: undefined, cold: 'Forever' }); + ).toEqual({ total: 'forever', hot: '34 days', warm: undefined, cold: 'forever' }); }); }); @@ -445,7 +445,7 @@ describe('Conversion of absolute policy timing to relative timing', () => { total: '61 days', hot: '24 days', warm: '37 days', - cold: 'Less than a day', + cold: 'less than a day', }); }); @@ -474,7 +474,7 @@ describe('Conversion of absolute policy timing to relative timing', () => { total: '61 days', hot: '61 days', warm: undefined, - cold: 'Less than a day', + cold: 'less than a day', }); }); @@ -506,8 +506,8 @@ describe('Conversion of absolute policy timing to relative timing', () => { ).toEqual({ total: '61 days', hot: '61 days', - warm: 'Less than a day', - cold: 'Less than a day', + warm: 'less than a day', + cold: 'less than a day', }); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts index 10c26702e81f1..73ff8c76b9233 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts @@ -35,11 +35,11 @@ type MinAgePhase = 'warm' | 'cold' | 'delete'; type Phase = 'hot' | MinAgePhase; const i18nTexts = { - forever: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.Forever', { - defaultMessage: 'Forever', + forever: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.forever', { + defaultMessage: 'forever', }), lessThanADay: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.lessThanADay', { - defaultMessage: 'Less than a day', + defaultMessage: 'less than a day', }), day: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.day', { defaultMessage: 'day', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 168eb14966493..c264e807ea234 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9401,7 +9401,6 @@ "xpack.idxMgmt.unfreezeIndicesAction.successfullyUnfrozeIndicesMessage": "[{indexNames}] の凍結が解除されました", "xpack.idxMgmt.updateIndexSettingsAction.settingsSuccessUpdateMessage": "インデックス {indexName} の設定が更新されました", "xpack.idxMgmt.validators.string.invalidJSONError": "無効な JSON フォーマット。", - "xpack.indexLifecycleMgmt.activePhaseMessage": "アクティブ", "xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel": "ライフサイクルポリシーを追加", "xpack.indexLifecycleMgmt.appTitle": "インデックスライフサイクルポリシー", "xpack.indexLifecycleMgmt.breadcrumb.editPolicyLabel": "ポリシーの編集", @@ -9450,8 +9449,6 @@ "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyLink": "新しいポリシーを作成", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyMessage": "既存のスナップショットポリシーの名前を入力するか、この名前で{link}。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyTitle": "ポリシー名が見つかりません", - "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescriptionText": "今後インデックスは必要ありません。 いつ安全に削除できるかを定義できます。", - "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseLabel": "削除フェーズ", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedLink": "スナップショットライフサイクルポリシーを作成", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedMessage": "{link}して、クラスタースナップショットの作成と削除を自動化します。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedTitle": "スナップショットポリシーが見つかりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 129deb575a52f..a2fe8e81e4635 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9425,7 +9425,6 @@ "xpack.idxMgmt.unfreezeIndicesAction.successfullyUnfrozeIndicesMessage": "成功取消冻结:[{indexNames}]", "xpack.idxMgmt.updateIndexSettingsAction.settingsSuccessUpdateMessage": "已成功更新索引 {indexName} 的设置", "xpack.idxMgmt.validators.string.invalidJSONError": "JSON 格式无效。", - "xpack.indexLifecycleMgmt.activePhaseMessage": "活动", "xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel": "添加生命周期策略", "xpack.indexLifecycleMgmt.appTitle": "索引生命周期策略", "xpack.indexLifecycleMgmt.breadcrumb.editPolicyLabel": "编辑策略", @@ -9474,8 +9473,6 @@ "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyLink": "创建新策略", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyMessage": "输入现有快照策略的名称,或使用此名称{link}。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyTitle": "未找到策略名称", - "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescriptionText": "您不再需要自己的索引。 您可以定义安全删除它的时间。", - "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseLabel": "删除阶段", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedLink": "创建快照生命周期策略", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedMessage": "{link}以自动创建和删除集群快照。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedTitle": "找不到快照策略", From 7b5d62fd551ec1c3d5455453eefb902779564097 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 9 Feb 2021 11:09:22 +0200 Subject: [PATCH 043/100] [Search Sessions] Enable extend from management (#90558) * Enable extend from management * fix extend jest test --- .../public/search/session/sessions_client.ts | 4 +-- .../components/actions/extend_button.tsx | 4 ++- .../search/sessions_mgmt/lib/api.test.ts | 16 +++++++++++- .../public/search/sessions_mgmt/lib/api.ts | 25 ++++++++++++------- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts index dcfc529f99b2b..1742db9d033bd 100644 --- a/src/plugins/data/public/search/session/sessions_client.ts +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -68,9 +68,9 @@ export class SessionsClient { }); } - public extend(sessionId: string, keepAlive: string): Promise { + public extend(sessionId: string, expires: string): Promise { return this.http!.post(`/internal/session/${encodeURIComponent(sessionId)}/_extend`, { - body: JSON.stringify({ keepAlive }), + body: JSON.stringify({ expires }), }); } diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx index 1e2678912ce99..06459db154f4a 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx @@ -58,7 +58,9 @@ const ExtendConfirm = ({ onCancel={onConfirmDismiss} onConfirm={async () => { setIsLoading(true); - await api.sendExtend(id, `${extendByDuration.asMilliseconds()}ms`); + await api.sendExtend(id, `${newExpiration.toISOString()}`); + setIsLoading(false); + onConfirmDismiss(); onActionComplete(); }} confirmButtonText={confirm} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts index 86acbcdb53001..0fa13ac145223 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts @@ -168,7 +168,7 @@ describe('Search Sessions Management API', () => { describe('extend', () => { beforeEach(() => { - sessionsClient.find = jest.fn().mockImplementation(async () => { + sessionsClient.extend = jest.fn().mockImplementation(async () => { return { saved_objects: [ { @@ -188,6 +188,20 @@ describe('Search Sessions Management API', () => { }); await api.sendExtend('my-id', '5d'); + expect(sessionsClient.extend).toHaveBeenCalledTimes(1); + expect(mockCoreStart.notifications.toasts.addSuccess).toHaveBeenCalled(); + }); + + test('displays error on reject', async () => { + sessionsClient.extend = jest.fn().mockRejectedValue({}); + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.sendExtend('my-id', '5d'); + + expect(sessionsClient.extend).toHaveBeenCalledTimes(1); expect(mockCoreStart.notifications.toasts.addError).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts index 264556f91cc37..42e9384cce2d8 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts @@ -166,9 +166,6 @@ export class SearchSessionsMgmtAPI { }), }); } catch (err) { - // eslint-disable-next-line no-console - console.error(err); - this.deps.notifications.toasts.addError(err, { title: i18n.translate('xpack.data.mgmt.searchSessions.api.deletedError', { defaultMessage: 'Failed to delete the search session!', @@ -178,11 +175,21 @@ export class SearchSessionsMgmtAPI { } // Extend - public async sendExtend(id: string, ttl: string): Promise { - this.deps.notifications.toasts.addError(new Error('Not implemented'), { - title: i18n.translate('xpack.data.mgmt.searchSessions.api.extendError', { - defaultMessage: 'Failed to extend the session expiration!', - }), - }); + public async sendExtend(id: string, expires: string): Promise { + try { + await this.sessionsClient.extend(id, expires); + + this.deps.notifications.toasts.addSuccess({ + title: i18n.translate('xpack.data.mgmt.searchSessions.api.extended', { + defaultMessage: 'The search session was extended.', + }), + }); + } catch (err) { + this.deps.notifications.toasts.addError(err, { + title: i18n.translate('xpack.data.mgmt.searchSessions.api.extendError', { + defaultMessage: 'Failed to extend the search session!', + }), + }); + } } } From a0d4b04155032a11eacfb445100490802e947d39 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 9 Feb 2021 12:28:43 +0200 Subject: [PATCH 044/100] [Security Solution][Case] ServiceNow SIR Connector (#88655) Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- .../servicenow/api.test.ts | 78 +++ .../builtin_action_types/servicenow/api.ts | 3 +- .../servicenow/index.test.ts | 120 +++++ .../builtin_action_types/servicenow/index.ts | 47 +- .../servicenow/translations.ts | 2 +- .../builtin_action_types/servicenow/types.ts | 1 + x-pack/plugins/case/common/api/cases/case.ts | 29 +- .../plugins/case/common/api/cases/comment.ts | 9 + .../case/common/api/cases/configure.ts | 4 - .../case/common/api/connectors/index.ts | 37 +- .../case/common/api/connectors/mappings.ts | 157 +----- .../{servicenow.ts => servicenow_itsm.ts} | 4 +- .../common/api/connectors/servicenow_sir.ts | 20 + x-pack/plugins/case/common/api/helpers.ts | 6 +- x-pack/plugins/case/common/constants.ts | 9 +- .../plugins/case/server/client/alerts/get.ts | 31 ++ .../case/server/client/alerts/types.ts | 19 + .../plugins/case/server/client/cases/get.ts | 45 ++ .../plugins/case/server/client/cases/mock.ts | 191 +++++++ .../plugins/case/server/client/cases/push.ts | 266 ++++++++++ .../plugins/case/server/client/cases/types.ts | 81 +++ .../configure => client/cases}/utils.test.ts | 467 ++++++++++++------ .../cases/configure => client/cases}/utils.ts | 238 ++++++--- .../case/server/client/configure/mock.ts | 4 +- .../case/server/client/configure/utils.ts | 10 +- .../plugins/case/server/client/index.test.ts | 86 ++-- x-pack/plugins/case/server/client/index.ts | 86 +--- x-pack/plugins/case/server/client/mocks.ts | 24 +- x-pack/plugins/case/server/client/types.ts | 51 +- .../case/server/client/user_actions/get.ts | 31 ++ .../case/server/connectors/case/index.ts | 3 +- .../plugins/case/server/connectors/index.ts | 48 +- .../jira/external_service_formatter.test.ts | 35 ++ .../jira/external_service_formatter.ts | 29 ++ .../external_service_formatter.test.ts | 26 + .../resilient/external_service_formatter.ts | 19 + .../connectors/servicenow/itsm_formatter.ts | 19 + .../servicenow/itsm_formmater.test.ts | 26 + .../servicenow/sir_formatter.test.ts | 164 ++++++ .../connectors/servicenow/sir_formatter.ts | 88 ++++ .../plugins/case/server/connectors/types.ts | 54 ++ x-pack/plugins/case/server/plugin.ts | 17 +- .../__fixtures__/create_mock_so_repository.ts | 14 + .../server/routes/api/__fixtures__/index.ts | 1 + .../api/__fixtures__/mock_actions_client.ts | 34 ++ .../api/__fixtures__/mock_saved_objects.ts | 47 +- .../routes/api/__fixtures__/route_contexts.ts | 25 +- .../routes/api/__mocks__/request_responses.ts | 29 +- .../api/cases/comments/delete_comment.test.ts | 8 +- .../api/cases/comments/get_comment.test.ts | 8 +- .../api/cases/comments/patch_comment.test.ts | 36 +- .../api/cases/comments/post_comment.test.ts | 40 +- .../api/cases/configure/get_configure.test.ts | 10 +- .../api/cases/configure/get_configure.ts | 4 +- .../cases/configure/get_connectors.test.ts | 16 +- .../api/cases/configure/get_connectors.ts | 13 +- .../server/routes/api/cases/configure/mock.ts | 76 --- .../cases/configure/patch_configure.test.ts | 14 +- .../api/cases/configure/patch_configure.ts | 2 +- .../cases/configure/post_configure.test.ts | 34 +- .../api/cases/configure/post_configure.ts | 4 +- .../configure/post_push_to_service.test.ts | 106 ---- .../cases/configure/post_push_to_service.ts | 81 --- .../routes/api/cases/delete_cases.test.ts | 16 +- .../routes/api/cases/find_cases.test.ts | 16 +- .../server/routes/api/cases/get_case.test.ts | 28 +- .../case/server/routes/api/cases/get_case.ts | 46 +- .../routes/api/cases/patch_cases.test.ts | 36 +- .../server/routes/api/cases/post_case.test.ts | 20 +- .../server/routes/api/cases/push_case.test.ts | 421 ++++++++++++++-- .../case/server/routes/api/cases/push_case.ts | 203 +------- .../api/cases/status/get_status.test.ts | 14 +- .../user_actions/get_all_user_actions.ts | 26 +- .../plugins/case/server/routes/api/index.ts | 6 +- .../plugins/case/server/routes/api/utils.ts | 18 +- .../case/server/services/alerts/index.ts | 44 ++ x-pack/plugins/case/server/services/mocks.ts | 1 + .../cases/connector_options.spec.ts | 7 + .../security_solution/cypress/objects/case.ts | 71 +++ .../cypress/screens/case_details.ts | 4 +- .../cypress/screens/edit_connector.ts | 2 +- .../cases/components/case_view/index.test.tsx | 13 +- .../cases/components/case_view/index.tsx | 2 - .../configure_cases/connectors.test.tsx | 4 +- .../components/configure_cases/index.test.tsx | 16 +- .../components/connector_selector/form.tsx | 18 +- .../{settings => connectors}/card.tsx | 6 +- .../case/{fields.tsx => alert_fields.tsx} | 0 .../cases/components/connectors/case/index.ts | 2 +- .../cases/components/connectors/config.ts | 34 +- .../connectors/connectors_registry.ts | 57 +++ .../{settings => connectors}/fields_form.tsx | 20 +- .../cases/components/connectors/index.ts | 46 ++ .../jira/__mocks__/api.ts | 0 .../{settings => connectors}/jira/api.test.ts | 0 .../{settings => connectors}/jira/api.ts | 0 .../jira/case_fields.test.tsx} | 2 +- .../jira/case_fields.tsx} | 19 +- .../{settings => connectors}/jira/index.ts | 6 +- .../jira/search_issues.tsx | 0 .../jira/translations.ts | 22 +- .../{settings => connectors}/jira/types.ts | 0 .../use_get_fields_by_issue_type.test.tsx | 0 .../jira/use_get_fields_by_issue_type.tsx | 0 .../jira/use_get_issue_types.test.tsx | 0 .../jira/use_get_issue_types.tsx | 0 .../jira/use_get_issues.test.tsx | 0 .../jira/use_get_issues.tsx | 0 .../jira/use_get_single_issue.test.tsx | 0 .../jira/use_get_single_issue.tsx | 0 .../cases/components/connectors/mock.ts | 109 ++++ .../resilient/__mocks__/api.ts | 21 +- .../{settings => connectors}/resilient/api.ts | 0 .../resilient/case_fields.test.tsx} | 2 +- .../resilient/case_fields.tsx} | 24 +- .../resilient/index.ts | 6 +- .../resilient/translations.ts | 10 +- .../resilient/types.ts | 0 .../resilient/use_get_incident_types.test.tsx | 0 .../resilient/use_get_incident_types.tsx | 0 .../resilient/use_get_severity.test.tsx | 0 .../resilient/use_get_severity.tsx | 0 .../connectors/servicenow/__mocks__/api.ts | 19 + .../connectors/servicenow/api.test.ts | 40 ++ .../components/connectors/servicenow/api.ts | 31 ++ .../components/connectors/servicenow/index.ts | 35 ++ .../servicenow_itsm_case_fields.test.tsx} | 62 ++- .../servicenow_itsm_case_fields.tsx | 164 ++++++ .../servicenow_sir_case_fields.test.tsx | 198 ++++++++ .../servicenow/servicenow_sir_case_fields.tsx | 293 +++++++++++ .../connectors/servicenow/translations.ts | 99 ++++ .../components/connectors/servicenow/types.ts | 18 + .../servicenow/use_get_choices.test.tsx | 144 ++++++ .../connectors/servicenow/use_get_choices.tsx | 99 ++++ .../cases/components/connectors/types.ts | 31 +- .../components/create/connector.test.tsx | 58 +-- .../cases/components/create/connector.tsx | 29 +- .../components/create/form_context.test.tsx | 62 +-- .../cases/components/create/form_context.tsx | 10 +- .../cases/components/create/index.test.tsx | 20 +- .../cases/components/edit_connector/index.tsx | 8 +- .../public/cases/components/settings/index.ts | 48 -- .../public/cases/components/settings/mock.ts | 21 - .../components/settings/servicenow/fields.tsx | 130 ----- .../components/settings/servicenow/index.ts | 25 - .../settings/servicenow/translations.ts | 49 -- .../components/settings/settings_registry.ts | 57 --- .../public/cases/components/settings/types.ts | 33 -- .../use_push_to_service/index.test.tsx | 38 +- .../components/use_push_to_service/index.tsx | 18 +- .../components/user_action_tree/index.tsx | 2 +- .../public/cases/containers/__mocks__/api.ts | 12 +- .../public/cases/containers/api.test.tsx | 106 ++-- .../public/cases/containers/api.ts | 39 +- .../public/cases/containers/mock.ts | 43 +- .../public/cases/containers/translations.ts | 7 - .../use_post_push_to_service.test.tsx | 304 +----------- .../containers/use_post_push_to_service.tsx | 219 ++------ .../public/cases/containers/utils.ts | 8 - .../translations/translations/ja-JP.json | 24 - .../translations/translations/zh-CN.json | 24 - .../builtin_action_types/jira/config.ts | 19 - .../builtin_action_types/jira/jira.tsx | 5 +- .../builtin_action_types/resilient/config.ts | 19 - .../resilient/resilient.tsx | 5 +- .../builtin_action_types/servicenow/config.ts | 31 -- .../servicenow/servicenow.tsx | 13 +- .../servicenow_itsm_params.test.tsx | 47 +- .../servicenow/translations.ts | 6 +- .../public/common/index.ts | 9 +- .../actions_simulators/server/plugin.ts | 3 + .../basic/tests/cases/push_case.ts | 167 +++++-- .../user_actions/get_all_user_actions.ts | 12 +- .../case_api_integration/common/lib/utils.ts | 4 +- 174 files changed, 4847 insertions(+), 2824 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts rename x-pack/plugins/case/common/api/connectors/{servicenow.ts => servicenow_itsm.ts} (76%) create mode 100644 x-pack/plugins/case/common/api/connectors/servicenow_sir.ts create mode 100644 x-pack/plugins/case/server/client/alerts/get.ts create mode 100644 x-pack/plugins/case/server/client/alerts/types.ts create mode 100644 x-pack/plugins/case/server/client/cases/get.ts create mode 100644 x-pack/plugins/case/server/client/cases/mock.ts create mode 100644 x-pack/plugins/case/server/client/cases/push.ts create mode 100644 x-pack/plugins/case/server/client/cases/types.ts rename x-pack/plugins/case/server/{routes/api/cases/configure => client/cases}/utils.test.ts (52%) rename x-pack/plugins/case/server/{routes/api/cases/configure => client/cases}/utils.ts (50%) create mode 100644 x-pack/plugins/case/server/client/user_actions/get.ts create mode 100644 x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts create mode 100644 x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts create mode 100644 x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.ts create mode 100644 x-pack/plugins/case/server/connectors/resilient/external_service_formatter.ts create mode 100644 x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts create mode 100644 x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts create mode 100644 x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts create mode 100644 x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts create mode 100644 x-pack/plugins/case/server/connectors/types.ts create mode 100644 x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts delete mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/mock.ts delete mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts delete mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/card.tsx (89%) rename x-pack/plugins/security_solution/public/cases/components/connectors/case/{fields.tsx => alert_fields.tsx} (100%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/connectors_registry.ts rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/fields_form.tsx (64%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/__mocks__/api.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/api.test.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/api.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings/jira/fields.test.tsx => connectors/jira/case_fields.test.tsx} (99%) rename x-pack/plugins/security_solution/public/cases/components/{settings/jira/fields.tsx => connectors/jira/case_fields.tsx} (91%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/index.ts (77%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/search_issues.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/translations.ts (60%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/types.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_fields_by_issue_type.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_fields_by_issue_type.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_issue_types.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_issue_types.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_issues.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_issues.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_single_issue.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_single_issue.tsx (100%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/__mocks__/api.ts (70%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/api.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings/resilient/fields.test.tsx => connectors/resilient/case_fields.test.tsx} (99%) rename x-pack/plugins/security_solution/public/cases/components/{settings/resilient/fields.tsx => connectors/resilient/case_fields.tsx} (90%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/index.ts (77%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/translations.ts (67%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/types.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/use_get_incident_types.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/use_get_incident_types.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/use_get_severity.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/use_get_severity.tsx (100%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/__mocks__/api.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.test.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts rename x-pack/plugins/security_solution/public/cases/components/{settings/servicenow/fields.test.tsx => connectors/servicenow/servicenow_itsm_case_fields.test.tsx} (52%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/index.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/mock.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/servicenow/index.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/servicenow/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/settings_registry.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/types.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 7ad6ec337bca1..662b1ce46a07b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -16,6 +16,7 @@ describe('api', () => { beforeEach(() => { externalService = externalServiceMock.create(); + jest.clearAllMocks(); }); describe('create incident', () => { @@ -26,6 +27,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(res).toEqual({ @@ -57,6 +59,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(res).toEqual({ @@ -77,6 +80,7 @@ describe('api', () => { params, secrets: { username: 'elastic', password: 'elastic' }, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(externalService.createIncident).toHaveBeenCalledWith({ @@ -99,6 +103,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(externalService.updateIncident).toHaveBeenCalledTimes(2); expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { @@ -125,6 +130,41 @@ describe('api', () => { incidentId: 'incident-1', }); }); + + test('it post comments to different comment field key', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ + externalService, + params, + secrets: {}, + logger: mockedLogger, + commentFieldKey: 'work_notes', + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(2); + expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + work_notes: 'A comment', + description: 'Incident description', + short_description: 'Incident title', + }, + incidentId: 'incident-1', + }); + + expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + work_notes: 'Another comment', + description: 'Incident description', + short_description: 'Incident title', + }, + incidentId: 'incident-1', + }); + }); }); describe('update incident', () => { @@ -134,6 +174,7 @@ describe('api', () => { params: apiParams, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(res).toEqual({ @@ -161,6 +202,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(res).toEqual({ @@ -178,6 +220,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(externalService.updateIncident).toHaveBeenCalledWith({ @@ -200,6 +243,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(externalService.updateIncident).toHaveBeenCalledTimes(3); expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { @@ -225,6 +269,40 @@ describe('api', () => { incidentId: 'incident-2', }); }); + + test('it post comments to different comment field key', async () => { + const params = { ...apiParams }; + await api.pushToService({ + externalService, + params, + secrets: {}, + logger: mockedLogger, + commentFieldKey: 'work_notes', + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(3); + expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + description: 'Incident description', + short_description: 'Incident title', + }, + incidentId: 'incident-3', + }); + + expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + work_notes: 'A comment', + description: 'Incident description', + short_description: 'Incident title', + }, + incidentId: 'incident-2', + }); + }); }); describe('getFields', () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index 3aa1e50dc2aeb..4120c07c32303 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -25,6 +25,7 @@ const pushToServiceHandler = async ({ externalService, params, secrets, + commentFieldKey, }: PushToServiceApiHandlerArgs): Promise => { const { comments } = params; let res: PushToServiceResponse; @@ -53,7 +54,7 @@ const pushToServiceHandler = async ({ incidentId: res.id, incident: { ...incident, - comments: currentComment.comment, + [commentFieldKey]: currentComment.comment, }, }); res.comments = [ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts new file mode 100644 index 0000000000000..e7e2b2bc4118e --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { actionsMock } from '../../mocks'; +import { createActionTypeRegistry } from '../index.test'; +import { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams, + PushToServiceResponse, +} from './types'; +import { + ServiceNowActionType, + ServiceNowITSMActionTypeId, + ServiceNowSIRActionTypeId, + ServiceNowActionTypeExecutorOptions, +} from '.'; +import { api } from './api'; + +jest.mock('./api', () => ({ + api: { + getChoices: jest.fn(), + getFields: jest.fn(), + getIncident: jest.fn(), + handshake: jest.fn(), + pushToService: jest.fn(), + }, +})); + +const services = actionsMock.createServices(); + +describe('ServiceNow', () => { + const config = { apiUrl: 'https://instance.com' }; + const secrets = { username: 'username', password: 'password' }; + const params = { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: 'An incident', + description: 'This is serious', + }, + }, + }; + + beforeEach(() => { + (api.pushToService as jest.Mock).mockResolvedValue({ id: 'some-id' }); + }); + + describe('ServiceNow ITSM', () => { + let actionType: ServiceNowActionType; + + beforeAll(() => { + const { actionTypeRegistry } = createActionTypeRegistry(); + actionType = actionTypeRegistry.get< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams, + PushToServiceResponse | {} + >(ServiceNowITSMActionTypeId); + }); + + describe('execute()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it pass the correct comment field key', async () => { + const actionId = 'some-action-id'; + const executorOptions = ({ + actionId, + config, + secrets, + params, + services, + } as unknown) as ServiceNowActionTypeExecutorOptions; + await actionType.executor(executorOptions); + expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe('comments'); + }); + }); + }); + + describe('ServiceNow SIR', () => { + let actionType: ServiceNowActionType; + + beforeAll(() => { + const { actionTypeRegistry } = createActionTypeRegistry(); + actionType = actionTypeRegistry.get< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams, + PushToServiceResponse | {} + >(ServiceNowSIRActionTypeId); + }); + + describe('execute()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it pass the correct comment field key', async () => { + const actionId = 'some-action-id'; + const executorOptions = ({ + actionId, + config, + secrets, + params, + services, + } as unknown) as ServiceNowActionTypeExecutorOptions; + await actionType.executor(executorOptions); + expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe( + 'work_notes' + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index cf9cef3c776c7..f6be7c90820a2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -47,15 +47,21 @@ const serviceNowSIRTable = 'sn_si_incident'; export const ServiceNowITSMActionTypeId = '.servicenow'; export const ServiceNowSIRActionTypeId = '.servicenow-sir'; -// action type definition -export function getServiceNowITSMActionType( - params: GetActionTypeParams -): ActionType< +export type ServiceNowActionType = ActionType< ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType, ExecutorParams, PushToServiceResponse | {} -> { +>; + +export type ServiceNowActionTypeExecutorOptions = ActionTypeExecutorOptions< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams +>; + +// action type definition +export function getServiceNowITSMActionType(params: GetActionTypeParams): ServiceNowActionType { const { logger, configurationUtilities } = params; return { id: ServiceNowITSMActionTypeId, @@ -74,14 +80,7 @@ export function getServiceNowITSMActionType( }; } -export function getServiceNowSIRActionType( - params: GetActionTypeParams -): ActionType< - ServiceNowPublicConfigurationType, - ServiceNowSecretConfigurationType, - ExecutorParams, - PushToServiceResponse | {} -> { +export function getServiceNowSIRActionType(params: GetActionTypeParams): ServiceNowActionType { const { logger, configurationUtilities } = params; return { id: ServiceNowSIRActionTypeId, @@ -96,7 +95,12 @@ export function getServiceNowSIRActionType( }), params: ExecutorParamsSchemaSIR, }, - executor: curry(executor)({ logger, configurationUtilities, table: serviceNowSIRTable }), + executor: curry(executor)({ + logger, + configurationUtilities, + table: serviceNowSIRTable, + commentFieldKey: 'work_notes', + }), }; } @@ -107,12 +111,14 @@ async function executor( logger, configurationUtilities, table, - }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; table: string }, - execOptions: ActionTypeExecutorOptions< - ServiceNowPublicConfigurationType, - ServiceNowSecretConfigurationType, - ExecutorParams - > + commentFieldKey = 'comments', + }: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; + table: string; + commentFieldKey?: string; + }, + execOptions: ServiceNowActionTypeExecutorOptions ): Promise> { const { actionId, config, params, secrets } = execOptions; const { subAction, subActionParams } = params; @@ -147,6 +153,7 @@ async function executor( params: pushToServiceParams, secrets, logger, + commentFieldKey, }); logger.debug(`response push to service for incident id: ${data.id}`); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 2110e9425fe6c..b46e118a7235f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -16,7 +16,7 @@ export const SERVICENOW_ITSM = i18n.translate('xpack.actions.builtin.serviceNowI }); export const SERVICENOW_SIR = i18n.translate('xpack.actions.builtin.serviceNowSIRTitle', { - defaultMessage: 'ServiceNow SIR', + defaultMessage: 'ServiceNow SecOps', }); export const ALLOWED_HOSTS_ERROR = (message: string) => diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 8de3f911106c0..1c0b2c9c62eee 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -121,6 +121,7 @@ export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerAr params: PushToServiceApiParams; secrets: Record; logger: Logger; + commentFieldKey: string; } export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index cd13b10846f12..bebd261fb7b9b 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -80,8 +80,6 @@ export const CasePostRequestRt = rt.type({ settings: SettingsRt, }); -export const CaseExternalServiceRequestRt = CaseExternalServiceBasicRt; - export const CasesFindRequestRt = rt.partial({ tags: rt.union([rt.array(rt.string), rt.string]), status: CaseStatusRt, @@ -126,6 +124,31 @@ export const CasePatchRequestRt = rt.intersection([ export const CasesPatchRequestRt = rt.type({ cases: rt.array(CasePatchRequestRt) }); export const CasesResponseRt = rt.array(CaseResponseRt); +export const CasePushRequestParamsRt = rt.type({ + case_id: rt.string, + connector_id: rt.string, +}); + +export const ExternalServiceResponseRt = rt.intersection([ + rt.type({ + title: rt.string, + id: rt.string, + pushedDate: rt.string, + url: rt.string, + }), + rt.partial({ + comments: rt.array( + rt.intersection([ + rt.type({ + commentId: rt.string, + pushedDate: rt.string, + }), + rt.partial({ externalCommentId: rt.string }), + ]) + ), + }), +]); + export type CaseAttributes = rt.TypeOf; export type CasePostRequest = rt.TypeOf; export type CaseResponse = rt.TypeOf; @@ -133,8 +156,8 @@ export type CasesResponse = rt.TypeOf; export type CasesFindResponse = rt.TypeOf; export type CasePatchRequest = rt.TypeOf; export type CasesPatchRequest = rt.TypeOf; -export type CaseExternalServiceRequest = rt.TypeOf; export type CaseFullExternalService = rt.TypeOf; +export type ExternalServiceResponse = rt.TypeOf; export type ESCaseAttributes = Omit & { connector: ESCaseConnector }; export type ESCasePatchRequest = Omit & { diff --git a/x-pack/plugins/case/common/api/cases/comment.ts b/x-pack/plugins/case/common/api/cases/comment.ts index 0670526e0df9c..7c9b31f496e54 100644 --- a/x-pack/plugins/case/common/api/cases/comment.ts +++ b/x-pack/plugins/case/common/api/cases/comment.ts @@ -45,6 +45,14 @@ export const CommentResponseRt = rt.intersection([ }), ]); +export const CommentResponseTypeAlertsRt = rt.intersection([ + AttributesTypeAlertsRt, + rt.type({ + id: rt.string, + version: rt.string, + }), +]); + export const AllCommentsResponseRT = rt.array(CommentResponseRt); export const CommentPatchRequestRt = rt.intersection([ @@ -84,6 +92,7 @@ export const AllCommentsResponseRt = rt.array(CommentResponseRt); export type CommentAttributes = rt.TypeOf; export type CommentRequest = rt.TypeOf; export type CommentResponse = rt.TypeOf; +export type CommentResponseAlertsType = rt.TypeOf; export type AllCommentsResponse = rt.TypeOf; export type CommentsResponse = rt.TypeOf; export type CommentPatchRequest = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts index cb3a8b68082dc..b5a89efde1767 100644 --- a/x-pack/plugins/case/common/api/cases/configure.ts +++ b/x-pack/plugins/case/common/api/cases/configure.ts @@ -7,13 +7,9 @@ import * as rt from 'io-ts'; -import { ActionResult, ActionType } from '../../../../actions/common'; import { UserRT } from '../user'; import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; -export type ActionConnector = ActionResult; -export type ActionTypeConnector = ActionType; - // TODO: we will need to add this type rt.literal('close-by-third-party') const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); diff --git a/x-pack/plugins/case/common/api/connectors/index.ts b/x-pack/plugins/case/common/api/connectors/index.ts index 5fead4c8bd9c5..f9b7c8b12c2cd 100644 --- a/x-pack/plugins/case/common/api/connectors/index.ts +++ b/x-pack/plugins/case/common/api/connectors/index.ts @@ -7,25 +7,34 @@ import * as rt from 'io-ts'; +import { ActionResult, ActionType } from '../../../../actions/common'; import { JiraFieldsRT } from './jira'; import { ResilientFieldsRT } from './resilient'; -import { ServiceNowFieldsRT } from './servicenow'; +import { ServiceNowITSMFieldsRT } from './servicenow_itsm'; +import { ServiceNowSIRFieldsRT } from './servicenow_sir'; export * from './jira'; -export * from './servicenow'; +export * from './servicenow_itsm'; +export * from './servicenow_sir'; export * from './resilient'; export * from './mappings'; +export type ActionConnector = ActionResult; +export type ActionTypeConnector = ActionType; + export const ConnectorFieldsRt = rt.union([ JiraFieldsRT, ResilientFieldsRT, - ServiceNowFieldsRT, + ServiceNowITSMFieldsRT, + ServiceNowSIRFieldsRT, rt.null, ]); + export enum ConnectorTypes { jira = '.jira', resilient = '.resilient', - servicenow = '.servicenow', + serviceNowITSM = '.servicenow', + serviceNowSIR = '.servicenow-sir', none = '.none', } @@ -39,9 +48,14 @@ const ConnectorResillientTypeFieldsRt = rt.type({ fields: rt.union([ResilientFieldsRT, rt.null]), }); -const ConnectorServiceNowTypeFieldsRt = rt.type({ - type: rt.literal(ConnectorTypes.servicenow), - fields: rt.union([ServiceNowFieldsRT, rt.null]), +const ConnectorServiceNowITSMTypeFieldsRt = rt.type({ + type: rt.literal(ConnectorTypes.serviceNowITSM), + fields: rt.union([ServiceNowITSMFieldsRT, rt.null]), +}); + +const ConnectorServiceNowSIRTypeFieldsRt = rt.type({ + type: rt.literal(ConnectorTypes.serviceNowSIR), + fields: rt.union([ServiceNowSIRFieldsRT, rt.null]), }); const ConnectorNoneTypeFieldsRt = rt.type({ @@ -52,7 +66,8 @@ const ConnectorNoneTypeFieldsRt = rt.type({ export const ConnectorTypeFieldsRt = rt.union([ ConnectorJiraTypeFieldsRt, ConnectorResillientTypeFieldsRt, - ConnectorServiceNowTypeFieldsRt, + ConnectorServiceNowITSMTypeFieldsRt, + ConnectorServiceNowSIRTypeFieldsRt, ConnectorNoneTypeFieldsRt, ]); @@ -66,6 +81,12 @@ export const CaseConnectorRt = rt.intersection([ export type CaseConnector = rt.TypeOf; export type ConnectorTypeFields = rt.TypeOf; +export type ConnectorJiraTypeFields = rt.TypeOf; +export type ConnectorResillientTypeFields = rt.TypeOf; +export type ConnectorServiceNowITSMTypeFields = rt.TypeOf< + typeof ConnectorServiceNowITSMTypeFieldsRt +>; +export type ConnectorServiceNowSIRTypeFields = rt.TypeOf; // we need to change these types back and forth for storing in ES (arrays overwrite, objects merge) export type ConnectorFields = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/connectors/mappings.ts b/x-pack/plugins/case/common/api/connectors/mappings.ts index 38e3434f0e7a8..3d2013af47688 100644 --- a/x-pack/plugins/case/common/api/connectors/mappings.ts +++ b/x-pack/plugins/case/common/api/connectors/mappings.ts @@ -5,42 +5,7 @@ * 2.0. */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - import * as rt from 'io-ts'; -import { - PushToServiceApiParams as JiraPushToServiceApiParams, - Incident as JiraIncident, -} from '../../../../actions/server/builtin_action_types/jira/types'; -import { - PushToServiceApiParams as ResilientPushToServiceApiParams, - Incident as ResilientIncident, -} from '../../../../actions/server/builtin_action_types/resilient/types'; -import { - PushToServiceApiParamsITSM as ServiceNowITSMPushToServiceApiParams, - ServiceNowITSMIncident, -} from '../../../../actions/server/builtin_action_types/servicenow/types'; -import { ResilientFieldsRT } from './resilient'; -import { ServiceNowFieldsRT } from './servicenow'; -import { JiraFieldsRT } from './jira'; - -// Formerly imported from security_solution -export interface ElasticUser { - readonly email?: string | null; - readonly fullName?: string | null; - readonly username?: string | null; -} - -export { - JiraPushToServiceApiParams, - ResilientPushToServiceApiParams, - ServiceNowITSMPushToServiceApiParams, -}; -export type Incident = JiraIncident | ResilientIncident | ServiceNowITSMIncident; -export type PushToServiceApiParams = - | JiraPushToServiceApiParams - | ResilientPushToServiceApiParams - | ServiceNowITSMPushToServiceApiParams; const ActionTypeRT = rt.union([ rt.literal('append'), @@ -52,6 +17,7 @@ const CaseFieldRT = rt.union([ rt.literal('description'), rt.literal('comments'), ]); + const ThirdPartyFieldRT = rt.union([rt.string, rt.literal('not_mapped')]); export type ActionType = rt.TypeOf; export type CaseField = rt.TypeOf; @@ -62,9 +28,11 @@ export const ConnectorMappingsAttributesRT = rt.type({ source: CaseFieldRT, target: ThirdPartyFieldRT, }); + export const ConnectorMappingsRt = rt.type({ mappings: rt.array(ConnectorMappingsAttributesRT), }); + export type ConnectorMappingsAttributes = rt.TypeOf; export type ConnectorMappings = rt.TypeOf; @@ -76,125 +44,12 @@ const ConnectorFieldRt = rt.type({ required: rt.boolean, type: FieldTypeRT, }); + export type ConnectorField = rt.TypeOf; -export const ConnectorRequestParamsRt = rt.type({ - connector_id: rt.string, -}); -export const GetFieldsRequestQueryRt = rt.type({ - connector_type: rt.string, -}); + const GetFieldsResponseRt = rt.type({ defaultMappings: rt.array(ConnectorMappingsAttributesRT), fields: rt.array(ConnectorFieldRt), }); -export type GetFieldsResponse = rt.TypeOf; - -export type ExternalServiceParams = Record; - -export interface PipedField { - actionType: string; - key: string; - pipes: string[]; - value: string; -} -export interface PrepareFieldsForTransformArgs { - defaultPipes: string[]; - mappings: ConnectorMappingsAttributes[]; - params: ServiceConnectorCaseParams; -} -export interface EntityInformation { - createdAt: string; - createdBy: ElasticUser; - updatedAt: string | null; - updatedBy: ElasticUser | null; -} -export interface TransformerArgs { - date?: string; - previousValue?: string; - user?: string; - value: string; -} - -export type Transformer = (args: TransformerArgs) => TransformerArgs; -export interface TransformFieldsArgs { - currentIncident?: S; - fields: PipedField[]; - params: P; -} - -export const ServiceConnectorUserParams = rt.type({ - fullName: rt.union([rt.string, rt.null]), - username: rt.string, -}); - -export const ServiceConnectorCommentParamsRt = rt.type({ - commentId: rt.string, - comment: rt.string, - createdAt: rt.string, - createdBy: ServiceConnectorUserParams, - updatedAt: rt.union([rt.string, rt.null]), - updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), -}); -export const ServiceConnectorBasicCaseParamsRt = rt.type({ - comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]), - createdAt: rt.string, - createdBy: ServiceConnectorUserParams, - description: rt.union([rt.string, rt.null]), - externalId: rt.union([rt.string, rt.null]), - savedObjectId: rt.string, - title: rt.string, - updatedAt: rt.union([rt.string, rt.null]), - updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), -}); - -export const ConnectorPartialFieldsRt = rt.partial({ - ...JiraFieldsRT.props, - ...ResilientFieldsRT.props, - ...ServiceNowFieldsRT.props, -}); - -export const ServiceConnectorCaseParamsRt = rt.intersection([ - ServiceConnectorBasicCaseParamsRt, - ConnectorPartialFieldsRt, -]); -export const ServiceConnectorCaseResponseRt = rt.intersection([ - rt.type({ - title: rt.string, - id: rt.string, - pushedDate: rt.string, - url: rt.string, - }), - rt.partial({ - comments: rt.array( - rt.intersection([ - rt.type({ - commentId: rt.string, - pushedDate: rt.string, - }), - rt.partial({ externalCommentId: rt.string }), - ]) - ), - }), -]); -export type ServiceConnectorBasicCaseParams = rt.TypeOf; -export type ServiceConnectorCaseParams = rt.TypeOf; -export type ServiceConnectorCaseResponse = rt.TypeOf; -export type ServiceConnectorCommentParams = rt.TypeOf; - -export const PostPushRequestRt = rt.type({ - connector_type: rt.string, - params: ServiceConnectorCaseParamsRt, -}); - -export type PostPushRequest = rt.TypeOf; - -export interface SimpleComment { - comment: string; - commentId: string; -} - -export interface MapIncident { - incident: ExternalServiceParams; - comments: SimpleComment[]; -} +export type GetFieldsResponse = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/connectors/servicenow.ts b/x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts similarity index 76% rename from x-pack/plugins/case/common/api/connectors/servicenow.ts rename to x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts index fc4e8f9aa09a3..2e86a26971aaa 100644 --- a/x-pack/plugins/case/common/api/connectors/servicenow.ts +++ b/x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts @@ -7,10 +7,10 @@ import * as rt from 'io-ts'; -export const ServiceNowFieldsRT = rt.type({ +export const ServiceNowITSMFieldsRT = rt.type({ impact: rt.union([rt.string, rt.null]), severity: rt.union([rt.string, rt.null]), urgency: rt.union([rt.string, rt.null]), }); -export type ServiceNowFieldsType = rt.TypeOf; +export type ServiceNowITSMFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts b/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts new file mode 100644 index 0000000000000..749abdea87437 --- /dev/null +++ b/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const ServiceNowSIRFieldsRT = rt.type({ + category: rt.union([rt.string, rt.null]), + destIp: rt.union([rt.boolean, rt.null]), + malwareHash: rt.union([rt.boolean, rt.null]), + malwareUrl: rt.union([rt.boolean, rt.null]), + priority: rt.union([rt.string, rt.null]), + sourceIp: rt.union([rt.boolean, rt.null]), + subcategory: rt.union([rt.string, rt.null]), +}); + +export type ServiceNowSIRFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts index f9de74f45de46..24c4756a1596b 100644 --- a/x-pack/plugins/case/common/api/helpers.ts +++ b/x-pack/plugins/case/common/api/helpers.ts @@ -10,7 +10,7 @@ import { CASE_COMMENTS_URL, CASE_USER_ACTIONS_URL, CASE_COMMENT_DETAILS_URL, - CASE_CONFIGURE_PUSH_URL, + CASE_PUSH_URL, } from '../constants'; export const getCaseDetailsUrl = (id: string): string => { @@ -28,6 +28,6 @@ export const getCaseCommentDetailsUrl = (caseId: string, commentId: string): str export const getCaseUserActionUrl = (id: string): string => { return CASE_USER_ACTIONS_URL.replace('{case_id}', id); }; -export const getCaseConfigurePushUrl = (id: string): string => { - return CASE_CONFIGURE_PUSH_URL.replace('{connector_id}', id); +export const getCasePushUrl = (caseId: string, connectorId: string): string => { + return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId); }; diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index 231ff9ef2dc4d..92dd2312f1ecf 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -15,10 +15,9 @@ export const CASES_URL = '/api/cases'; export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}`; export const CASE_CONFIGURE_URL = `${CASES_URL}/configure`; export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`; -export const CASE_CONFIGURE_CONNECTOR_DETAILS_URL = `${CASE_CONFIGURE_CONNECTORS_URL}/{connector_id}`; -export const CASE_CONFIGURE_PUSH_URL = `${CASE_CONFIGURE_CONNECTOR_DETAILS_URL}/push`; export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`; export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`; +export const CASE_PUSH_URL = `${CASE_DETAILS_URL}/connector/{connector_id}/_push`; export const CASE_REPORTERS_URL = `${CASES_URL}/reporters`; export const CASE_STATUS_URL = `${CASES_URL}/status`; export const CASE_TAGS_URL = `${CASES_URL}/tags`; @@ -30,12 +29,14 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; export const ACTION_URL = '/api/actions'; export const ACTION_TYPES_URL = '/api/actions/list_action_types'; -export const SERVICENOW_ACTION_TYPE_ID = '.servicenow'; +export const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow'; +export const SERVICENOW_SIR_ACTION_TYPE_ID = '.servicenow-sir'; export const JIRA_ACTION_TYPE_ID = '.jira'; export const RESILIENT_ACTION_TYPE_ID = '.resilient'; export const SUPPORTED_CONNECTORS = [ - SERVICENOW_ACTION_TYPE_ID, + SERVICENOW_ITSM_ACTION_TYPE_ID, + SERVICENOW_SIR_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID, ]; diff --git a/x-pack/plugins/case/server/client/alerts/get.ts b/x-pack/plugins/case/server/client/alerts/get.ts new file mode 100644 index 0000000000000..718dd327aa08c --- /dev/null +++ b/x-pack/plugins/case/server/client/alerts/get.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { CaseClientGetAlerts, CaseClientFactoryArguments } from '../types'; +import { CaseClientGetAlertsResponse } from './types'; + +export const get = ({ alertsService, request, context }: CaseClientFactoryArguments) => async ({ + ids, +}: CaseClientGetAlerts): Promise => { + const securitySolutionClient = context?.securitySolution?.getAppClient(); + if (securitySolutionClient == null) { + throw Boom.notFound('securitySolutionClient client have not been found'); + } + + if (ids.length === 0) { + return []; + } + + const index = securitySolutionClient.getSignalsIndex(); + const alerts = await alertsService.getAlerts({ ids, index, request }); + return alerts.hits.hits.map((alert) => ({ + id: alert._id, + index: alert._index, + ...alert._source, + })); +}; diff --git a/x-pack/plugins/case/server/client/alerts/types.ts b/x-pack/plugins/case/server/client/alerts/types.ts new file mode 100644 index 0000000000000..7b9d4a8856f48 --- /dev/null +++ b/x-pack/plugins/case/server/client/alerts/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +interface Alert { + id: string; + index: string; + destination?: { + ip: string; + }; + source?: { + ip: string; + }; +} + +export type CaseClientGetAlertsResponse = Alert[]; diff --git a/x-pack/plugins/case/server/client/cases/get.ts b/x-pack/plugins/case/server/client/cases/get.ts new file mode 100644 index 0000000000000..c1901ccaae511 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/get.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { flattenCaseSavedObject } from '../../routes/api/utils'; +import { CaseResponseRt, CaseResponse } from '../../../common/api'; +import { CaseClientGet, CaseClientFactoryArguments } from '../types'; + +export const get = ({ savedObjectsClient, caseService }: CaseClientFactoryArguments) => async ({ + id, + includeComments = false, +}: CaseClientGet): Promise => { + const theCase = await caseService.getCase({ + client: savedObjectsClient, + caseId: id, + }); + + if (!includeComments) { + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: theCase, + }) + ); + } + + const theComments = await caseService.getAllCaseComments({ + client: savedObjectsClient, + caseId: id, + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + }); + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: theCase, + comments: theComments.saved_objects, + totalComment: theComments.total, + }) + ); +}; diff --git a/x-pack/plugins/case/server/client/cases/mock.ts b/x-pack/plugins/case/server/client/cases/mock.ts new file mode 100644 index 0000000000000..57e2d4373a52b --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/mock.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CommentResponse, + CommentType, + ConnectorMappingsAttributes, + CaseUserActionsResponse, +} from '../../../common/api'; + +import { BasicParams } from './types'; + +export const updateUser = { + updated_at: '2020-03-13T08:34:53.450Z', + updated_by: { full_name: 'Another User', username: 'another', email: 'elastic@elastic.co' }, +}; + +const entity = { + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { full_name: 'Elastic User', username: 'elastic', email: 'elastic@elastic.co' }, + updatedAt: null, + updatedBy: null, +}; + +export const comment: CommentResponse = { + id: 'mock-comment-1', + comment: 'Wow, good luck catching that bad meanie!', + type: CommentType.user as const, + created_at: '2019-11-25T21:55:00.177Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + updated_at: '2019-11-25T21:55:00.177Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: 'WzEsMV0=', +}; + +export const commentAlert: CommentResponse = { + id: 'mock-comment-1', + alertId: 'alert-id-1', + index: 'alert-index-1', + type: CommentType.alert as const, + created_at: '2019-11-25T21:55:00.177Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + updated_at: '2019-11-25T21:55:00.177Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: 'WzEsMV0=', +}; + +export const defaultPipes = ['informationCreated']; +export const basicParams: BasicParams = { + description: 'a description', + title: 'a title', + ...entity, +}; + +export const mappings: ConnectorMappingsAttributes[] = [ + { + source: 'title', + target: 'short_description', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'append', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, +]; + +export const userActions: CaseUserActionsResponse = [ + { + action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'], + action: 'create', + action_at: '2021-02-03T17:41:03.771Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + '{"title":"Case SIR","tags":["sir"],"description":"testing sir","connector":{"id":"456","name":"ServiceNow SN","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', + old_value: null, + action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: null, + }, + { + action_field: ['pushed'], + action: 'push-to-service', + action_at: '2021-02-03T17:41:26.108Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + '{"pushed_at":"2021-02-03T17:41:26.108Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"456","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + old_value: null, + action_id: '0a801750-6647-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: null, + }, + { + action_field: ['comment'], + action: 'create', + action_at: '2021-02-03T17:44:21.067Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: '{"type":"alert","alertId":"alert-id-1","index":".siem-signals-default-000008"}', + old_value: null, + action_id: '7373eb60-6647-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: 'comment-alert-1', + }, + { + action_field: ['comment'], + action: 'create', + action_at: '2021-02-03T17:44:33.078Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: '{"type":"alert","alertId":"alert-id-2","index":".siem-signals-default-000008"}', + old_value: null, + action_id: '7abc6410-6647-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: 'comment-alert-2', + }, + { + action_field: ['pushed'], + action: 'push-to-service', + action_at: '2021-02-03T17:45:29.400Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"456","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + old_value: null, + action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: null, + }, + { + action_field: ['comment'], + action: 'create', + action_at: '2021-02-03T17:48:30.616Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: '{"comment":"a comment!","type":"user"}', + old_value: null, + action_id: '0818e5e0-6648-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: 'comment-user-1', + }, +]; diff --git a/x-pack/plugins/case/server/client/cases/push.ts b/x-pack/plugins/case/server/client/cases/push.ts new file mode 100644 index 0000000000000..f329fb4d00d07 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/push.ts @@ -0,0 +1,266 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom, { isBoom, Boom as BoomType } from '@hapi/boom'; + +import { SavedObjectsBulkUpdateResponse, SavedObjectsUpdateResponse } from 'kibana/server'; +import { flattenCaseSavedObject } from '../../routes/api/utils'; + +import { + ActionConnector, + CaseResponseRt, + CaseResponse, + CaseStatuses, + ExternalServiceResponse, + ESCaseAttributes, + CommentAttributes, +} from '../../../common/api'; +import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; + +import { CaseClientPush, CaseClientFactoryArguments } from '../types'; +import { createIncident, getCommentContextFromAttributes, isCommentAlertType } from './utils'; + +const createError = (e: Error | BoomType, message: string): Error | BoomType => { + if (isBoom(e)) { + e.message = message; + e.output.payload.message = message; + return e; + } + + return Error(message); +}; + +export const push = ({ + savedObjectsClient, + caseService, + caseConfigureService, + userActionService, + request, + response, +}: CaseClientFactoryArguments) => async ({ + actionsClient, + caseClient, + caseId, + connectorId, +}: CaseClientPush): Promise => { + /* Start of push to external service */ + let theCase; + let connector; + let userActions; + let alerts; + let connectorMappings; + let externalServiceIncident; + + try { + [theCase, connector, userActions] = await Promise.all([ + caseClient.get({ id: caseId, includeComments: true }), + actionsClient.get({ id: connectorId }), + caseClient.getUserActions({ caseId }), + ]); + } catch (e) { + const message = `Error getting case and/or connector and/or user actions: ${e.message}`; + throw createError(e, message); + } + + // We need to change the logic when we support subcases + if (theCase?.status === CaseStatuses.closed) { + throw Boom.conflict( + `This case ${theCase.title} is closed. You can not pushed if the case is closed.` + ); + } + + try { + alerts = await caseClient.getAlerts({ + ids: theCase?.comments?.filter(isCommentAlertType).map((comment) => comment.alertId) ?? [], + }); + } catch (e) { + throw new Error(`Error getting alerts for case with id ${theCase.id}: ${e.message}`); + } + + try { + connectorMappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: connector.id, + connectorType: connector.actionTypeId, + }); + } catch (e) { + const message = `Error getting mapping for connector with id ${connector.id}: ${e.message}`; + throw createError(e, message); + } + + try { + externalServiceIncident = await createIncident({ + actionsClient, + theCase, + userActions, + connector: connector as ActionConnector, + mappings: connectorMappings, + alerts, + }); + } catch (e) { + const message = `Error creating incident for case with id ${theCase.id}: ${e.message}`; + throw createError(e, message); + } + + const pushRes = await actionsClient.execute({ + actionId: connector?.id ?? '', + params: { + subAction: 'pushToService', + subActionParams: externalServiceIncident, + }, + }); + + if (pushRes.status === 'error') { + throw Boom.failedDependency( + pushRes.serviceMessage ?? pushRes.message ?? 'Error pushing to service' + ); + } + + /* End of push to external service */ + + /* Start of update case with push information */ + let user; + let myCase; + let myCaseConfigure; + let comments; + + try { + [user, myCase, myCaseConfigure, comments] = await Promise.all([ + caseService.getUser({ request, response }), + caseService.getCase({ + client: savedObjectsClient, + caseId, + }), + caseConfigureService.find({ client: savedObjectsClient }), + caseService.getAllCaseComments({ + client: savedObjectsClient, + caseId, + options: { + fields: [], + page: 1, + perPage: theCase?.totalComment ?? 0, + }, + }), + ]); + } catch (e) { + const message = `Error getting user and/or case and/or case configuration and/or case comments: ${e.message}`; + throw createError(e, message); + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const pushedDate = new Date().toISOString(); + const externalServiceResponse = pushRes.data as ExternalServiceResponse; + + const externalService = { + pushed_at: pushedDate, + pushed_by: { username, full_name, email }, + connector_id: connector.id, + connector_name: connector.name, + external_id: externalServiceResponse.id, + external_title: externalServiceResponse.title, + external_url: externalServiceResponse.url, + }; + + let updatedCase: SavedObjectsUpdateResponse; + let updatedComments: SavedObjectsBulkUpdateResponse; + + try { + [updatedCase, updatedComments] = await Promise.all([ + caseService.patchCase({ + client: savedObjectsClient, + caseId, + updatedAttributes: { + ...(myCaseConfigure.total > 0 && + myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing' + ? { + status: CaseStatuses.closed, + closed_at: pushedDate, + closed_by: { email, full_name, username }, + } + : {}), + external_service: externalService, + updated_at: pushedDate, + updated_by: { username, full_name, email }, + }, + version: myCase.version, + }), + + caseService.patchComments({ + client: savedObjectsClient, + comments: comments.saved_objects + .filter((comment) => comment.attributes.pushed_at == null) + .map((comment) => ({ + commentId: comment.id, + updatedAttributes: { + pushed_at: pushedDate, + pushed_by: { username, full_name, email }, + }, + version: comment.version, + })), + }), + + userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + ...(myCaseConfigure.total > 0 && + myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing' + ? [ + buildCaseUserActionItem({ + action: 'update', + actionAt: pushedDate, + actionBy: { username, full_name, email }, + caseId, + fields: ['status'], + newValue: CaseStatuses.closed, + oldValue: myCase.attributes.status, + }), + ] + : []), + buildCaseUserActionItem({ + action: 'push-to-service', + actionAt: pushedDate, + actionBy: { username, full_name, email }, + caseId, + fields: ['pushed'], + newValue: JSON.stringify(externalService), + }), + ], + }), + ]); + } catch (e) { + const message = `Error updating case and/or comments and/or creating user action: ${e.message}`; + throw createError(e, message); + } + /* End of update case with push information */ + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + }, + comments: comments.saved_objects.map((origComment) => { + const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); + return { + ...origComment, + ...updatedComment, + attributes: { + ...origComment.attributes, + ...updatedComment?.attributes, + ...getCommentContextFromAttributes(origComment.attributes), + }, + version: updatedComment?.version ?? origComment.version, + references: origComment?.references ?? [], + }; + }), + }) + ); +}; diff --git a/x-pack/plugins/case/server/client/cases/types.ts b/x-pack/plugins/case/server/client/cases/types.ts new file mode 100644 index 0000000000000..f1d56e7132bd1 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/types.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { + PushToServiceApiParams as JiraPushToServiceApiParams, + Incident as JiraIncident, +} from '../../../../actions/server/builtin_action_types/jira/types'; +import { + PushToServiceApiParams as ResilientPushToServiceApiParams, + Incident as ResilientIncident, +} from '../../../../actions/server/builtin_action_types/resilient/types'; +import { + PushToServiceApiParamsITSM as ServiceNowITSMPushToServiceApiParams, + PushToServiceApiParamsSIR as ServiceNowSIRPushToServiceApiParams, + ServiceNowITSMIncident, +} from '../../../../actions/server/builtin_action_types/servicenow/types'; +import { CaseResponse, ConnectorMappingsAttributes } from '../../../common/api'; + +export type Incident = JiraIncident | ResilientIncident | ServiceNowITSMIncident; +export type PushToServiceApiParams = + | JiraPushToServiceApiParams + | ResilientPushToServiceApiParams + | ServiceNowITSMPushToServiceApiParams + | ServiceNowSIRPushToServiceApiParams; + +export type ExternalServiceParams = Record; + +export interface BasicParams { + title: CaseResponse['title']; + description: CaseResponse['description']; + createdAt: CaseResponse['created_at']; + createdBy: CaseResponse['created_by']; + updatedAt: CaseResponse['updated_at']; + updatedBy: CaseResponse['updated_by']; +} + +export interface PipedField { + actionType: string; + key: string; + pipes: string[]; + value: string; +} +export interface PrepareFieldsForTransformArgs { + defaultPipes: string[]; + mappings: ConnectorMappingsAttributes[]; + params: { title: string; description: string }; +} +export interface EntityInformation { + createdAt: CaseResponse['created_at']; + createdBy: CaseResponse['created_by']; + updatedAt: CaseResponse['updated_at']; + updatedBy: CaseResponse['updated_by']; +} +export interface TransformerArgs { + date?: string; + previousValue?: string; + user?: string; + value: string; +} + +export type Transformer = (args: TransformerArgs) => TransformerArgs; +export interface TransformFieldsArgs { + currentIncident?: S; + fields: PipedField[]; + params: P; +} + +export interface ExternalServiceComment { + comment: string; + commentId: string; +} + +export interface MapIncident { + incident: ExternalServiceParams; + comments: ExternalServiceComment[]; +} diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts b/x-pack/plugins/case/server/client/cases/utils.test.ts similarity index 52% rename from x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts rename to x-pack/plugins/case/server/client/cases/utils.test.ts index 5114703c60963..dca2c34602678 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts +++ b/x-pack/plugins/case/server/client/cases/utils.test.ts @@ -5,34 +5,45 @@ * 2.0. */ +import { actionsClientMock } from '../../../../actions/server/actions_client.mock'; +import { flattenCaseSavedObject } from '../../routes/api/utils'; +import { mockCases } from '../../routes/api/__fixtures__'; + +import { BasicParams, ExternalServiceParams, Incident } from './types'; +import { + comment as commentObj, + mappings, + defaultPipes, + basicParams, + userActions, + commentAlert, +} from './mock'; + import { - mapIncident, + createIncident, + getLatestPushInfo, prepareFieldsForTransformation, - serviceFormatter, transformComments, transformers, transformFields, } from './utils'; -import { comment as commentObj, mappings, defaultPipes, params, updateUser } from './mock'; -import { - ConnectorTypes, - ExternalServiceParams, - Incident, - ServiceConnectorCaseParams, -} from '../../../../../common/api/connectors'; -import { actionsClientMock } from '../../../../../../actions/server/actions_client.mock'; -import { mappings as mappingsMock } from '../../../../client/configure/mock'; -const formatComment = { commentId: commentObj.commentId, comment: commentObj.comment }; -const serviceNowParams = params[ConnectorTypes.servicenow] as ServiceConnectorCaseParams; -describe('api/cases/configure/utils', () => { +const formatComment = { + commentId: commentObj.id, + comment: 'Wow, good luck catching that bad meanie!', +}; + +const params = { ...basicParams }; + +describe('utils', () => { describe('prepareFieldsForTransformation', () => { test('prepare fields with defaults', () => { const res = prepareFieldsForTransformation({ defaultPipes, - params: serviceNowParams, + params, mappings, }); + expect(res).toEqual([ { actionType: 'overwrite', @@ -53,8 +64,9 @@ describe('api/cases/configure/utils', () => { const res = prepareFieldsForTransformation({ defaultPipes: ['myTestPipe'], mappings, - params: serviceNowParams, + params, }); + expect(res).toEqual([ { actionType: 'overwrite', @@ -71,16 +83,17 @@ describe('api/cases/configure/utils', () => { ]); }); }); + describe('transformFields', () => { test('transform fields for creation correctly', () => { const fields = prepareFieldsForTransformation({ defaultPipes, mappings, - params: serviceNowParams, + params, }); - const res = transformFields({ - params: serviceNowParams, + const res = transformFields({ + params, fields, }); @@ -92,18 +105,19 @@ describe('api/cases/configure/utils', () => { test('transform fields for update correctly', () => { const fields = prepareFieldsForTransformation({ - params: serviceNowParams, + params, mappings, defaultPipes: ['informationUpdated'], }); - const res = transformFields({ + const res = transformFields({ params: { - ...serviceNowParams, + ...params, updatedAt: '2020-03-15T08:34:53.450Z', updatedBy: { username: 'anotherUser', - fullName: 'Another User', + full_name: 'Another User', + email: 'elastic@elastic.co', }, }, fields, @@ -112,6 +126,7 @@ describe('api/cases/configure/utils', () => { description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', }, }); + expect(res).toEqual({ short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', description: @@ -121,13 +136,13 @@ describe('api/cases/configure/utils', () => { test('add newline character to description', () => { const fields = prepareFieldsForTransformation({ - params: serviceNowParams, + params, mappings, defaultPipes: ['informationUpdated'], }); - const res = transformFields({ - params: serviceNowParams, + const res = transformFields({ + params, fields, currentIncident: { short_description: 'first title', @@ -141,13 +156,13 @@ describe('api/cases/configure/utils', () => { const fields = prepareFieldsForTransformation({ defaultPipes, mappings, - params: serviceNowParams, + params, }); - const res = transformFields({ + const res = transformFields({ params: { - ...serviceNowParams, - createdBy: { fullName: '', username: 'elastic' }, + ...params, + createdBy: { full_name: '', username: 'elastic', email: 'elastic@elastic.co' }, }, fields, }); @@ -162,14 +177,14 @@ describe('api/cases/configure/utils', () => { const fields = prepareFieldsForTransformation({ defaultPipes: ['informationUpdated'], mappings, - params: serviceNowParams, + params, }); - const res = transformFields({ + const res = transformFields({ params: { - ...serviceNowParams, + ...params, updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: '' }, + updatedBy: { username: 'anotherUser', full_name: '', email: 'elastic@elastic.co' }, }, fields, }); @@ -180,6 +195,7 @@ describe('api/cases/configure/utils', () => { }); }); }); + describe('transformComments', () => { test('transform creation comments', () => { const comments = [commentObj]; @@ -187,7 +203,7 @@ describe('api/cases/configure/utils', () => { expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (created at ${comments[0].createdAt} by ${comments[0].createdBy.fullName})`, + comment: `${formatComment.comment} (created at ${comments[0].created_at} by ${comments[0].created_by.full_name})`, }, ]); }); @@ -196,14 +212,19 @@ describe('api/cases/configure/utils', () => { const comments = [ { ...commentObj, - ...updateUser, + updated_at: '2020-03-13T08:34:53.450Z', + updated_by: { + full_name: 'Another User', + username: 'another', + email: 'elastic@elastic.co', + }, }, ]; const res = transformComments(comments, ['informationUpdated']); expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (updated at ${updateUser.updatedAt} by ${updateUser.updatedBy.fullName})`, + comment: `${formatComment.comment} (updated at ${comments[0].updated_at} by ${comments[0].updated_by.full_name})`, }, ]); }); @@ -214,19 +235,19 @@ describe('api/cases/configure/utils', () => { expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (added at ${comments[0].createdAt} by ${comments[0].createdBy.fullName})`, + comment: `${formatComment.comment} (added at ${comments[0].created_at} by ${comments[0].created_by.full_name})`, }, ]); }); test('transform comments without fullname', () => { - const comments = [{ ...commentObj, createdBy: { username: commentObj.createdBy.username } }]; - // @ts-ignore testing no fullName + const comments = [{ ...commentObj, createdBy: { username: commentObj.created_by.username } }]; + // @ts-ignore testing no full_name const res = transformComments(comments, ['informationAdded']); expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (added at ${comments[0].createdAt} by ${comments[0].createdBy.username})`, + comment: `${formatComment.comment} (added at ${comments[0].created_at} by ${comments[0].created_by.username})`, }, ]); }); @@ -235,15 +256,15 @@ describe('api/cases/configure/utils', () => { const comments = [ { ...commentObj, - updatedAt: '2020-04-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic2', username: 'elastic' }, + updated_at: '2020-04-13T08:34:53.450Z', + updated_by: { full_name: 'Elastic2', username: 'elastic', email: 'elastic@elastic.co' }, }, ]; const res = transformComments(comments, ['informationAdded']); expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (added at ${comments[0].updatedAt} by ${comments[0].updatedBy.fullName})`, + comment: `${formatComment.comment} (added at ${comments[0].updated_at} by ${comments[0].updated_by.full_name})`, }, ]); }); @@ -252,19 +273,20 @@ describe('api/cases/configure/utils', () => { const comments = [ { ...commentObj, - updatedAt: '2020-04-13T08:34:53.450Z', - updatedBy: { fullName: '', username: 'elastic2' }, + updated_at: '2020-04-13T08:34:53.450Z', + updated_by: { full_name: '', username: 'elastic2', email: 'elastic@elastic.co' }, }, ]; const res = transformComments(comments, ['informationAdded']); expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (added at ${comments[0].updatedAt} by ${comments[0].updatedBy.username})`, + comment: `${formatComment.comment} (added at ${comments[0].updated_at} by ${comments[0].updated_by.username})`, }, ]); }); }); + describe('transformers', () => { const { informationCreated, informationUpdated, informationAdded, append } = transformers; describe('informationCreated', () => { @@ -389,142 +411,291 @@ describe('api/cases/configure/utils', () => { }); }); }); - describe('mapIncident', () => { + + describe('createIncident', () => { let actionsMock = actionsClientMock.create(); - it('maps an external incident', async () => { - const res = await mapIncident( - actionsMock, - '123', - ConnectorTypes.servicenow, - mappingsMock[ConnectorTypes.servicenow], - serviceNowParams - ); + const theCase = { + ...flattenCaseSavedObject({ + savedObject: mockCases[0], + }), + comments: [commentObj], + totalComments: 1, + }; + + const connector = { + id: '456', + actionTypeId: '.jira', + name: 'Connector without isCaseOwned', + config: { + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + }; + + it('creates an external incident', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase, + userActions: [], + connector, + mappings, + alerts: [], + }); + expect(res).toEqual({ incident: { - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + priority: null, + labels: ['defacement'], + issueType: null, + parent: null, + short_description: + 'Super Bad Security Issue (created at 2019-11-25T21:54:48.952Z by elastic)', + description: + 'This is a brand new case of a bad meanie defacing data (created at 2019-11-25T21:54:48.952Z by elastic)', externalId: null, - impact: '3', - severity: '1', - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - urgency: '2', }, - comments: [ + comments: [], + }); + }); + + it('it creates comments correctly', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase: { + ...theCase, + comments: [{ ...commentObj, id: 'comment-user-1' }], + }, + userActions, + connector, + mappings, + alerts: [], + }); + + expect(res.comments).toEqual([ + { + comment: + 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-user-1', + }, + ]); + }); + + it('it does NOT creates comments when mapping is nothing', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase: { + ...theCase, + comments: [{ ...commentObj, id: 'comment-user-1' }], + }, + userActions, + connector, + mappings: [ + mappings[0], + mappings[1], { - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + source: 'comments', + target: 'comments', + action_type: 'nothing', }, ], + alerts: [], }); + + expect(res.comments).toEqual([]); }); - it('throws error if invalid service', async () => { - await mapIncident( - actionsMock, - '123', - 'invalid', - mappingsMock[ConnectorTypes.servicenow], - serviceNowParams - ).catch((e) => { - expect(e).not.toBeNull(); - expect(e).toEqual(new Error(`Invalid service`)); + + it('it creates comments of type alert correctly', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase: { + ...theCase, + comments: [ + { ...commentObj, id: 'comment-user-1' }, + { ...commentAlert, id: 'comment-alert-1' }, + { ...commentAlert, id: 'comment-alert-2' }, + ], + }, + // Remove second push + userActions: userActions.filter((item, index) => index !== 4), + connector, + mappings: [ + ...mappings, + { + source: 'comments', + target: 'comments', + action_type: 'nothing', + }, + ], + alerts: [], }); + + expect(res.comments).toEqual([ + { + comment: + 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-user-1', + }, + { + comment: + 'Alert with id alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-alert-1', + }, + { + comment: + 'Alert with id alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-alert-2', + }, + ]); }); + it('updates an existing incident', async () => { const existingIncidentData = { - description: 'fun description', - impact: '3', - severity: '3', + priority: null, + issueType: null, + parent: null, short_description: 'fun title', - urgency: '3', + description: 'fun description', }; + const execute = jest.fn().mockReturnValue(existingIncidentData); actionsMock = { ...actionsMock, execute }; - const res = await mapIncident( - actionsMock, - '123', - ConnectorTypes.servicenow, - mappingsMock[ConnectorTypes.servicenow], - { ...serviceNowParams, externalId: '123' } - ); + + const res = await createIncident({ + actionsClient: actionsMock, + theCase, + userActions, + connector, + mappings, + alerts: [], + }); + expect(res).toEqual({ incident: { - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - externalId: '123', - impact: '3', - severity: '1', - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - urgency: '2', + priority: null, + labels: ['defacement'], + issueType: null, + parent: null, + description: + 'fun description \r\nThis is a brand new case of a bad meanie defacing data (updated at 2019-11-25T21:54:48.952Z by elastic)', + externalId: 'external-id', + short_description: + 'Super Bad Security Issue (updated at 2019-11-25T21:54:48.952Z by elastic)', }, - comments: [ - { - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - }, - ], + comments: [], }); }); + it('throws error when existing incident throws', async () => { + expect.assertions(2); const execute = jest.fn().mockImplementation(() => { throw new Error('exception'); }); + actionsMock = { ...actionsMock, execute }; - await mapIncident( - actionsMock, - '123', - ConnectorTypes.servicenow, - mappingsMock[ConnectorTypes.servicenow], - { ...serviceNowParams, externalId: '123' } - ).catch((e) => { + createIncident({ + actionsClient: actionsMock, + theCase, + userActions, + connector, + mappings, + alerts: [], + }).catch((e) => { expect(e).not.toBeNull(); expect(e).toEqual( new Error( - `Retrieving Incident by id 123 from ServiceNow failed with exception: Error: exception` + `Retrieving Incident by id external-id from .jira failed with exception: Error: exception` ) ); }); }); - }); - const connectors = [ - { - name: ConnectorTypes.jira, - result: { - incident: { - issueType: '10003', - parent: '5002', - priority: 'Highest', - }, - thirdPartyName: 'Jira', - }, - }, - { - name: ConnectorTypes.resilient, - result: { - incident: { - incidentTypes: ['10003'], - severityCode: '1', - }, - thirdPartyName: 'Resilient', - }, - }, - { - name: ConnectorTypes.servicenow, - result: { - incident: { - impact: '3', - severity: '1', - urgency: '2', - }, - thirdPartyName: 'ServiceNow', - }, - }, - ]; - describe('serviceFormatter', () => { - connectors.forEach((c) => - it(`formats ${c.name}`, () => { - const caseParams = params[c.name] as ServiceConnectorCaseParams; - const res = serviceFormatter(c.name, caseParams); - expect(res).toEqual(c.result); - }) - ); + it('throws error if connector is not supported', async () => { + expect.assertions(2); + createIncident({ + actionsClient: actionsMock, + theCase, + userActions, + connector: { ...connector, actionTypeId: 'not-supported' }, + mappings, + alerts: [], + }).catch((e) => { + expect(e).not.toBeNull(); + expect(e).toEqual(new Error('Invalid external service')); + }); + }); + + describe('getLatestPushInfo', () => { + it('it returns the latest push information correctly', async () => { + const res = getLatestPushInfo('456', userActions); + expect(res).toEqual({ + index: 4, + pushedInfo: { + connector_id: '456', + connector_name: 'ServiceNow SN', + external_id: 'external-id', + external_title: 'SIR0010037', + external_url: + 'https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id', + pushed_at: '2021-02-03T17:45:29.400Z', + pushed_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + }, + }); + }); + + it('it returns null when there are not actions', async () => { + const res = getLatestPushInfo('456', []); + expect(res).toBe(null); + }); + + it('it returns null when there are no push user action', async () => { + const res = getLatestPushInfo('456', [userActions[0]]); + expect(res).toBe(null); + }); + + it('it returns the correct push information when with multiple push on different connectors', async () => { + const res = getLatestPushInfo('456', [ + ...userActions.slice(0, 3), + { + action_field: ['pushed'], + action: 'push-to-service', + action_at: '2021-02-03T17:45:29.400Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + // The connector id is 123 + '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"123","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + old_value: null, + action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: null, + }, + ]); + + expect(res).toEqual({ + index: 1, + pushedInfo: { + connector_id: '456', + connector_name: 'ServiceNow SN', + external_id: 'external-id', + external_title: 'SIR0010037', + external_url: + 'https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id', + pushed_at: '2021-02-03T17:41:26.108Z', + pushed_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + }, + }); + }); + }); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts similarity index 50% rename from x-pack/plugins/case/server/routes/api/cases/configure/utils.ts rename to x-pack/plugins/case/server/client/cases/utils.ts index 01a1a580bd78f..6974fd4ffa288 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts +++ b/x-pack/plugins/case/server/client/cases/utils.ts @@ -8,46 +8,118 @@ import { i18n } from '@kbn/i18n'; import { flow } from 'lodash'; import { - ServiceConnectorCaseParams, - ServiceConnectorCommentParams, + ActionConnector, + CaseResponse, + CaseFullExternalService, + CaseUserActionsResponse, + CommentResponse, + CommentResponseAlertsType, + CommentType, ConnectorMappingsAttributes, ConnectorTypes, + CommentAttributes, + CommentRequestUserType, + CommentRequestAlertType, +} from '../../../common/api'; +import { ActionsClient } from '../../../../actions/server'; +import { externalServiceFormatters, FormatterConnectorTypes } from '../../connectors'; +import { CaseClientGetAlertsResponse } from '../../client/alerts/types'; +import { + BasicParams, EntityInformation, ExternalServiceParams, + ExternalServiceComment, Incident, - JiraPushToServiceApiParams, MapIncident, PipedField, PrepareFieldsForTransformArgs, PushToServiceApiParams, - ResilientPushToServiceApiParams, - ServiceNowITSMPushToServiceApiParams, - SimpleComment, Transformer, TransformerArgs, TransformFieldsArgs, -} from '../../../../../common/api'; -import { ActionsClient } from '../../../../../../actions/server'; -export const mapIncident = async ( - actionsClient: ActionsClient, +} from './types'; + +export const getLatestPushInfo = ( connectorId: string, - connectorType: string, - mappings: ConnectorMappingsAttributes[], - params: ServiceConnectorCaseParams -): Promise => { - const { comments: caseComments, externalId } = params; + userActions: CaseUserActionsResponse +): { index: number; pushedInfo: CaseFullExternalService } | null => { + for (const [index, action] of [...userActions].reverse().entries()) { + if (action.action === 'push-to-service' && action.new_value) + try { + const pushedInfo = JSON.parse(action.new_value); + if (pushedInfo.connector_id === connectorId) { + // We returned the index of the element in the userActions array. + // As we traverse the userActions in reverse we need to calculate the index of a normal traversal + return { index: userActions.length - index - 1, pushedInfo }; + } + } catch (e) { + // Silence JSON parse errors + } + } + + return null; +}; + +const isConnectorSupported = (connectorId: string): connectorId is FormatterConnectorTypes => + Object.values(ConnectorTypes).includes(connectorId as ConnectorTypes); + +const getCommentContent = (comment: CommentResponse): string => { + if (comment.type === CommentType.user) { + return comment.comment; + } else if (comment.type === CommentType.alert) { + return `Alert with id ${comment.alertId} added to case`; + } + + return ''; +}; + +interface CreateIncidentArgs { + actionsClient: ActionsClient; + theCase: CaseResponse; + userActions: CaseUserActionsResponse; + connector: ActionConnector; + mappings: ConnectorMappingsAttributes[]; + alerts: CaseClientGetAlertsResponse; +} + +export const createIncident = async ({ + actionsClient, + theCase, + userActions, + connector, + mappings, + alerts, +}: CreateIncidentArgs): Promise => { + const { + comments: caseComments, + title, + description, + created_at: createdAt, + created_by: createdBy, + updated_at: updatedAt, + updated_by: updatedBy, + } = theCase; + + if (!isConnectorSupported(connector.actionTypeId)) { + throw new Error('Invalid external service'); + } + + const params = { title, description, createdAt, createdBy, updatedAt, updatedBy }; + const latestPushInfo = getLatestPushInfo(connector.id, userActions); + const externalId = latestPushInfo?.pushedInfo?.external_id ?? null; const defaultPipes = externalId ? ['informationUpdated'] : ['informationCreated']; let currentIncident: ExternalServiceParams | undefined; - const service = serviceFormatter(connectorType, params); - if (service == null) { - throw new Error(`Invalid service`); - } - const thirdPartyName = service.thirdPartyName; - let incident: Partial = service.incident; + + const externalServiceFields = externalServiceFormatters[connector.actionTypeId].format( + theCase, + alerts + ); + let incident: Partial = { ...externalServiceFields }; + if (externalId) { try { currentIncident = ((await actionsClient.execute({ - actionId: connectorId, + actionId: connector.id, params: { subAction: 'getIncident', subActionParams: { externalId }, @@ -55,80 +127,56 @@ export const mapIncident = async ( })) as unknown) as ExternalServiceParams | undefined; } catch (ex) { throw new Error( - `Retrieving Incident by id ${externalId} from ${thirdPartyName} failed with exception: ${ex}` + `Retrieving Incident by id ${externalId} from ${connector.actionTypeId} failed with exception: ${ex}` ); } } + const fields = prepareFieldsForTransformation({ defaultPipes, mappings, params, }); - const transformedFields = transformFields< - ServiceConnectorCaseParams, - ExternalServiceParams, - Incident - >({ + + const transformedFields = transformFields({ params, fields, currentIncident, }); + incident = { ...incident, ...transformedFields, externalId }; - let comments: SimpleComment[] = []; - if (caseComments && Array.isArray(caseComments) && caseComments.length > 0) { + + const commentsIdsToBeUpdated = new Set( + userActions + .slice(latestPushInfo?.index ?? 0) + .filter( + (action, index) => + Array.isArray(action.action_field) && action.action_field[0] === 'comment' + ) + .map((action) => action.comment_id) + ); + const commentsToBeUpdated = caseComments?.filter((comment) => + commentsIdsToBeUpdated.has(comment.id) + ); + + let comments: ExternalServiceComment[] = []; + if (commentsToBeUpdated && Array.isArray(commentsToBeUpdated) && commentsToBeUpdated.length > 0) { const commentsMapping = mappings.find((m) => m.source === 'comments'); if (commentsMapping?.action_type !== 'nothing') { - comments = transformComments(caseComments, ['informationAdded']); + comments = transformComments(commentsToBeUpdated, ['informationAdded']); } } return { incident, comments }; }; -export const serviceFormatter = ( - connectorType: string, - params: unknown -): { thirdPartyName: string; incident: Partial } | null => { - switch (connectorType) { - case ConnectorTypes.jira: - const { - priority, - labels, - issueType, - parent, - } = params as JiraPushToServiceApiParams['incident']; - return { - incident: { priority, labels, issueType, parent }, - thirdPartyName: 'Jira', - }; - case ConnectorTypes.resilient: - const { incidentTypes, severityCode } = params as ResilientPushToServiceApiParams['incident']; - return { - incident: { incidentTypes, severityCode }, - thirdPartyName: 'Resilient', - }; - case ConnectorTypes.servicenow: - const { - severity, - urgency, - impact, - } = params as ServiceNowITSMPushToServiceApiParams['incident']; - return { - incident: { severity, urgency, impact }, - thirdPartyName: 'ServiceNow', - }; - default: - return null; - } -}; - export const getEntity = (entity: EntityInformation): string => (entity.updatedBy != null - ? entity.updatedBy.fullName - ? entity.updatedBy.fullName + ? entity.updatedBy.full_name + ? entity.updatedBy.full_name : entity.updatedBy.username : entity.createdBy != null - ? entity.createdBy.fullName - ? entity.createdBy.fullName + ? entity.createdBy.full_name + ? entity.createdBy.full_name : entity.createdBy.username : '') ?? ''; @@ -160,6 +208,7 @@ export const FIELD_INFORMATION = ( }); } }; + export const transformers: Record = { informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ value: `${value} ${FIELD_INFORMATION('create', date, user)}`, @@ -178,6 +227,7 @@ export const transformers: Record = { ...rest, }), }; + export const prepareFieldsForTransformation = ({ defaultPipes, mappings, @@ -226,14 +276,46 @@ export const transformFields = < }; export const transformComments = ( - comments: ServiceConnectorCommentParams[], + comments: CaseResponse['comments'] = [], pipes: string[] -): SimpleComment[] => +): ExternalServiceComment[] => comments.map((c) => ({ comment: flow(...pipes.map((p) => transformers[p]))({ - value: c.comment, - date: c.updatedAt ?? c.createdAt, - user: getEntity(c), + value: getCommentContent(c), + date: c.updated_at ?? c.created_at, + user: getEntity({ + createdAt: c.created_at, + createdBy: c.created_by, + updatedAt: c.updated_at, + updatedBy: c.updated_by, + }), }).value, - commentId: c.commentId, + commentId: c.id, })); + +export const isCommentAlertType = ( + comment: CommentResponse +): comment is CommentResponseAlertsType => comment.type === CommentType.alert; + +export const getCommentContextFromAttributes = ( + attributes: CommentAttributes +): CommentRequestUserType | CommentRequestAlertType => { + switch (attributes.type) { + case CommentType.user: + return { + type: CommentType.user, + comment: attributes.comment, + }; + case CommentType.alert: + return { + type: CommentType.alert, + alertId: attributes.alertId, + index: attributes.index, + }; + default: + return { + type: CommentType.user, + comment: '', + }; + } +}; diff --git a/x-pack/plugins/case/server/client/configure/mock.ts b/x-pack/plugins/case/server/client/configure/mock.ts index 46df0a7ac6756..4d0c384e23e27 100644 --- a/x-pack/plugins/case/server/client/configure/mock.ts +++ b/x-pack/plugins/case/server/client/configure/mock.ts @@ -70,7 +70,7 @@ export const mappings: TestMappings = { action_type: 'append', }, ], - [ConnectorTypes.servicenow]: [ + [ConnectorTypes.serviceNowITSM]: [ { source: 'title', target: 'short_description', @@ -611,7 +611,7 @@ export const formatFieldsTestData: FormatFieldsTestData[] = [ { id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' }, ], fields: serviceNowFields, - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, }, ]; export const mockGetFieldsResponse = { diff --git a/x-pack/plugins/case/server/client/configure/utils.ts b/x-pack/plugins/case/server/client/configure/utils.ts index 2fc9e3d17801c..7e91c2ae5a4d7 100644 --- a/x-pack/plugins/case/server/client/configure/utils.ts +++ b/x-pack/plugins/case/server/client/configure/utils.ts @@ -70,7 +70,9 @@ export const formatFields = (theData: unknown, theType: string): ConnectorField[ return normalizeJiraFields(theData as JiraGetFieldsResponse); case ConnectorTypes.resilient: return normalizeResilientFields(theData as ResilientGetFieldsResponse); - case ConnectorTypes.servicenow: + case ConnectorTypes.serviceNowITSM: + return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse); + case ConnectorTypes.serviceNowSIR: return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse); default: return []; @@ -97,10 +99,14 @@ const getPreferredFields = (theType: string) => { } else if (theType === ConnectorTypes.resilient) { title = 'name'; description = 'description'; - } else if (theType === ConnectorTypes.servicenow) { + } else if ( + theType === ConnectorTypes.serviceNowITSM || + theType === ConnectorTypes.serviceNowSIR + ) { title = 'short_description'; description = 'description'; } + return { title, description }; }; diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts index 095dc5102b720..4daa4d1c0bd8b 100644 --- a/x-pack/plugins/case/server/client/index.test.ts +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest, kibanaResponseFactory } from '../../../../../src/core/server'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { createCaseClient } from '.'; import { @@ -17,29 +17,48 @@ import { } from '../services/mocks'; import { create } from './cases/create'; +import { get } from './cases/get'; import { update } from './cases/update'; +import { push } from './cases/push'; import { addComment } from './comments/add'; +import { getFields } from './configure/get_fields'; +import { getMappings } from './configure/get_mappings'; import { updateAlertsStatus } from './alerts/update_status'; +import { get as getUserActions } from './user_actions/get'; +import { get as getAlerts } from './alerts/get'; import type { CasesRequestHandlerContext } from '../types'; jest.mock('./cases/create'); jest.mock('./cases/update'); +jest.mock('./cases/get'); +jest.mock('./cases/push'); jest.mock('./comments/add'); jest.mock('./alerts/update_status'); +jest.mock('./alerts/get'); +jest.mock('./user_actions/get'); +jest.mock('./configure/get_fields'); +jest.mock('./configure/get_mappings'); const caseConfigureService = createConfigureServiceMock(); const alertsService = createAlertServiceMock(); const caseService = createCaseServiceMock(); const connectorMappingsService = connectorMappingsServiceMock(); const request = {} as KibanaRequest; +const response = kibanaResponseFactory; const savedObjectsClient = savedObjectsClientMock.create(); const userActionService = createUserActionServiceMock(); const context = {} as CasesRequestHandlerContext; const createMock = create as jest.Mock; +const getMock = get as jest.Mock; const updateMock = update as jest.Mock; +const pushMock = push as jest.Mock; const addCommentMock = addComment as jest.Mock; const updateAlertsStatusMock = updateAlertsStatus as jest.Mock; +const getAlertsStatusMock = getAlerts as jest.Mock; +const getFieldsMock = getFields as jest.Mock; +const getMappingsMock = getMappings as jest.Mock; +const getUserActionsMock = getUserActions as jest.Mock; describe('createCaseClient()', () => { test('it creates the client correctly', async () => { @@ -50,49 +69,34 @@ describe('createCaseClient()', () => { connectorMappingsService, context, request, + response, savedObjectsClient, userActionService, }); - expect(createMock).toHaveBeenCalledWith({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }); - - expect(updateMock).toHaveBeenCalledWith({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }); - - expect(addCommentMock).toHaveBeenCalledWith({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }); - - expect(updateAlertsStatusMock).toHaveBeenCalledWith({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - context, - request, - savedObjectsClient, - userActionService, - }); + [ + createMock, + getMock, + updateMock, + pushMock, + addCommentMock, + updateAlertsStatusMock, + getAlertsStatusMock, + getFieldsMock, + getMappingsMock, + getUserActionsMock, + ].forEach((method) => + expect(method).toHaveBeenCalledWith({ + caseConfigureService, + caseService, + connectorMappingsService, + request, + response, + savedObjectsClient, + userActionService, + alertsService, + context, + }) + ); }); }); diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts index 1b9d3ce7ecb08..e15b9fc766562 100644 --- a/x-pack/plugins/case/server/client/index.ts +++ b/x-pack/plugins/case/server/client/index.ts @@ -5,73 +5,41 @@ * 2.0. */ -import { CaseClientFactoryArguments, CaseClient } from './types'; +import { + CaseClientFactoryArguments, + CaseClient, + CaseClientFactoryMethods, + CaseClientMethods, +} from './types'; import { create } from './cases/create'; +import { get } from './cases/get'; import { update } from './cases/update'; +import { push } from './cases/push'; import { addComment } from './comments/add'; import { getFields } from './configure/get_fields'; import { getMappings } from './configure/get_mappings'; import { updateAlertsStatus } from './alerts/update_status'; +import { get as getUserActions } from './user_actions/get'; +import { get as getAlerts } from './alerts/get'; export { CaseClient } from './types'; -export const createCaseClient = ({ - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - alertsService, - context, -}: CaseClientFactoryArguments): CaseClient => { - return { - create: create({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }), - update: update({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }), - addComment: addComment({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }), - getFields: getFields(), - getMappings: getMappings({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }), - updateAlertsStatus: updateAlertsStatus({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - context, - request, - savedObjectsClient, - userActionService, - }), +export const createCaseClient = (args: CaseClientFactoryArguments): CaseClient => { + const methods: CaseClientFactoryMethods = { + create, + get, + update, + push, + addComment, + getAlerts, + getFields, + getMappings, + getUserActions, + updateAlertsStatus, }; + + return (Object.keys(methods) as CaseClientMethods[]).reduce((client, method) => { + client[method] = methods[method](args); + return client; + }, {} as CaseClient); }; diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts index 0d7f3972e58e7..b2a07e36b3aed 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -6,9 +6,9 @@ */ import { omit } from 'lodash/fp'; -import { KibanaRequest } from 'kibana/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaRequest, kibanaResponseFactory } from '../../../../../src/core/server/http'; import { loggingSystemMock } from '../../../../../src/core/server/mocks'; -import { actionsClientMock } from '../../../actions/server/mocks'; import { AlertServiceContract, CaseConfigureService, @@ -17,17 +17,20 @@ import { ConnectorMappingsService, } from '../services'; import { CaseClient } from './types'; -import { authenticationMock } from '../routes/api/__fixtures__'; +import { authenticationMock, createActionsClient } from '../routes/api/__fixtures__'; import { createCaseClient } from '.'; -import { getActions } from '../routes/api/__mocks__/request_responses'; import type { CasesRequestHandlerContext } from '../types'; export type CaseClientMock = jest.Mocked; export const createCaseClientMock = (): CaseClientMock => ({ addComment: jest.fn(), create: jest.fn(), + get: jest.fn(), + push: jest.fn(), + getAlerts: jest.fn(), getFields: jest.fn(), getMappings: jest.fn(), + getUserActions: jest.fn(), update: jest.fn(), updateAlertsStatus: jest.fn(), }); @@ -47,10 +50,10 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ alertsService: jest.Mocked; }; }> => { - const actionsMock = actionsClientMock.create(); - actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); + const actionsMock = createActionsClient(); const log = loggingSystemMock.create().get('case'); const request = {} as KibanaRequest; + const response = kibanaResponseFactory; const caseServicePlugin = new CaseService(log); const caseConfigureServicePlugin = new CaseConfigureService(log); @@ -63,11 +66,15 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ const connectorMappingsService = await connectorMappingsServicePlugin.setup(); const userActionService = { - postUserActions: jest.fn(), getUserActions: jest.fn(), + postUserActions: jest.fn(), }; - const alertsService = { initialize: jest.fn(), updateAlertsStatus: jest.fn() }; + const alertsService = { + initialize: jest.fn(), + updateAlertsStatus: jest.fn(), + getAlerts: jest.fn(), + }; const context = { core: { @@ -89,6 +96,7 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ const caseClient = createCaseClient({ savedObjectsClient, request, + response, caseService, caseConfigureService, connectorMappingsService, diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index a3466e26294f8..8778aa46a2d24 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { KibanaRequest, KibanaResponseFactory, SavedObjectsClientContract } from 'kibana/server'; import { ActionsClient } from '../../../actions/server'; import { CasePostRequest, @@ -16,6 +16,7 @@ import { CommentRequest, ConnectorMappingsAttributes, GetFieldsResponse, + CaseUserActionsResponse, } from '../../common/api'; import { CaseConfigureServiceSetup, @@ -25,6 +26,7 @@ import { } from '../services'; import { ConnectorMappingsServiceSetup } from '../services/connector_mappings'; import type { CasesRequestHandlerContext } from '../types'; +import { CaseClientGetAlertsResponse } from './alerts/types'; export interface CaseClientCreate { theCase: CasePostRequest; @@ -35,6 +37,18 @@ export interface CaseClientUpdate { cases: CasesPatchRequest; } +export interface CaseClientGet { + id: string; + includeComments?: boolean; +} + +export interface CaseClientPush { + actionsClient: ActionsClient; + caseClient: CaseClient; + caseId: string; + connectorId: string; +} + export interface CaseClientAddComment { caseClient: CaseClient; caseId: string; @@ -46,11 +60,27 @@ export interface CaseClientUpdateAlertsStatus { status: CaseStatuses; } +export interface CaseClientGetAlerts { + ids: string[]; +} + +export interface CaseClientGetUserActions { + caseId: string; +} + +export interface MappingsClient { + actionsClient: ActionsClient; + caseClient: CaseClient; + connectorId: string; + connectorType: string; +} + export interface CaseClientFactoryArguments { caseConfigureService: CaseConfigureServiceSetup; caseService: CaseServiceSetup; connectorMappingsService: ConnectorMappingsServiceSetup; request: KibanaRequest; + response: KibanaResponseFactory; savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; @@ -65,15 +95,22 @@ export interface ConfigureFields { export interface CaseClient { addComment: (args: CaseClientAddComment) => Promise; create: (args: CaseClientCreate) => Promise; + get: (args: CaseClientGet) => Promise; + getAlerts: (args: CaseClientGetAlerts) => Promise; getFields: (args: ConfigureFields) => Promise; getMappings: (args: MappingsClient) => Promise; + getUserActions: (args: CaseClientGetUserActions) => Promise; + push: (args: CaseClientPush) => Promise; update: (args: CaseClientUpdate) => Promise; updateAlertsStatus: (args: CaseClientUpdateAlertsStatus) => Promise; } -export interface MappingsClient { - actionsClient: ActionsClient; - caseClient: CaseClient; - connectorId: string; - connectorType: string; -} +export type CaseClientFactoryMethod = ( + factoryArgs: CaseClientFactoryArguments +) => (methodArgs: any) => Promise; + +export type CaseClientMethods = keyof CaseClient; + +export type CaseClientFactoryMethods = { + [K in CaseClientMethods]: CaseClientFactoryMethod; +}; diff --git a/x-pack/plugins/case/server/client/user_actions/get.ts b/x-pack/plugins/case/server/client/user_actions/get.ts new file mode 100644 index 0000000000000..e83a9e3484262 --- /dev/null +++ b/x-pack/plugins/case/server/client/user_actions/get.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; +import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api'; +import { CaseClientGetUserActions, CaseClientFactoryArguments } from '../types'; + +export const get = ({ + savedObjectsClient, + userActionService, +}: CaseClientFactoryArguments) => async ({ + caseId, +}: CaseClientGetUserActions): Promise => { + const userActions = await userActionService.getUserActions({ + client: savedObjectsClient, + caseId, + }); + + return CaseUserActionsResponseRt.encode( + userActions.saved_objects.map((ua) => ({ + ...ua.attributes, + action_id: ua.id, + case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '', + comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null, + })) + ); +}; diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index 01446942c33c6..9907aa5b3cd3a 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -7,7 +7,7 @@ import { curry } from 'lodash'; -import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest, kibanaResponseFactory } from '../../../../../../src/core/server'; import { ActionTypeExecutorResult } from '../../../../actions/common'; import { CasePatchRequest, CasePostRequest } from '../../../common/api'; import { createCaseClient } from '../../client'; @@ -73,6 +73,7 @@ async function executor( const caseClient = createCaseClient({ savedObjectsClient, request: {} as KibanaRequest, + response: kibanaResponseFactory, caseService, caseConfigureService, connectorMappingsService, diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts index 100511e271b02..00809d81ca5f2 100644 --- a/x-pack/plugins/case/server/connectors/index.ts +++ b/x-pack/plugins/case/server/connectors/index.ts @@ -5,43 +5,14 @@ * 2.0. */ -import { Logger } from 'kibana/server'; -import { - ActionTypeConfig, - ActionTypeSecrets, - ActionTypeParams, - ActionType, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../actions/server/types'; -import { - CaseServiceSetup, - CaseConfigureServiceSetup, - CaseUserActionServiceSetup, - ConnectorMappingsServiceSetup, - AlertServiceContract, -} from '../services'; - +import { RegisterConnectorsArgs, ExternalServiceFormatterMapper } from './types'; import { getActionType as getCaseConnector } from './case'; +import { serviceNowITSMExternalServiceFormatter } from './servicenow/itsm_formatter'; +import { serviceNowSIRExternalServiceFormatter } from './servicenow/sir_formatter'; +import { jiraExternalServiceFormatter } from './jira/external_service_formatter'; +import { resilientExternalServiceFormatter } from './resilient/external_service_formatter'; -export interface GetActionTypeParams { - logger: Logger; - caseService: CaseServiceSetup; - caseConfigureService: CaseConfigureServiceSetup; - connectorMappingsService: ConnectorMappingsServiceSetup; - userActionService: CaseUserActionServiceSetup; - alertsService: AlertServiceContract; -} - -export interface RegisterConnectorsArgs extends GetActionTypeParams { - actionsRegisterType< - Config extends ActionTypeConfig = ActionTypeConfig, - Secrets extends ActionTypeSecrets = ActionTypeSecrets, - Params extends ActionTypeParams = ActionTypeParams, - ExecutorResultData = void - >( - actionType: ActionType - ): void; -} +export * from './types'; export const registerConnectors = ({ actionsRegisterType, @@ -63,3 +34,10 @@ export const registerConnectors = ({ }) ); }; + +export const externalServiceFormatters: ExternalServiceFormatterMapper = { + '.servicenow': serviceNowITSMExternalServiceFormatter, + '.servicenow-sir': serviceNowSIRExternalServiceFormatter, + '.jira': jiraExternalServiceFormatter, + '.resilient': resilientExternalServiceFormatter, +}; diff --git a/x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts new file mode 100644 index 0000000000000..0bfaf7cdbd9e3 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseResponse } from '../../../common/api'; +import { jiraExternalServiceFormatter } from './external_service_formatter'; + +describe('Jira formatter', () => { + const theCase = { + tags: ['tag'], + connector: { fields: { priority: 'High', issueType: 'Task', parent: null } }, + } as CaseResponse; + + it('it formats correctly', async () => { + const res = await jiraExternalServiceFormatter.format(theCase, []); + expect(res).toEqual({ ...theCase.connector.fields, labels: theCase.tags }); + }); + + it('it formats correctly when fields do not exist ', async () => { + const invalidFields = { tags: ['tag'], connector: { fields: null } } as CaseResponse; + const res = await jiraExternalServiceFormatter.format(invalidFields, []); + expect(res).toEqual({ priority: null, issueType: null, parent: null, labels: theCase.tags }); + }); + + it('it replace white spaces with hyphens on tags', async () => { + const res = await jiraExternalServiceFormatter.format( + { ...theCase, tags: ['a tag with spaces'] }, + [] + ); + expect(res).toEqual({ ...theCase.connector.fields, labels: ['a-tag-with-spaces'] }); + }); +}); diff --git a/x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts new file mode 100644 index 0000000000000..74376d295fea5 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { JiraFieldsType, ConnectorJiraTypeFields } from '../../../common/api'; +import { ExternalServiceFormatter } from '../types'; + +interface ExternalServiceParams extends JiraFieldsType { + labels: string[]; +} + +const format: ExternalServiceFormatter['format'] = (theCase) => { + const { priority = null, issueType = null, parent = null } = + (theCase.connector.fields as ConnectorJiraTypeFields['fields']) ?? {}; + return { + priority, + // Jira do not allows empty spaces on labels. We replace white spaces with hyphens + labels: theCase.tags.map((tag) => tag.replace(/\s+/g, '-')), + issueType, + parent, + }; +}; + +export const jiraExternalServiceFormatter: ExternalServiceFormatter = { + format, +}; diff --git a/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.ts b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.ts new file mode 100644 index 0000000000000..01280e9692b5e --- /dev/null +++ b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseResponse } from '../../../common/api'; +import { resilientExternalServiceFormatter } from './external_service_formatter'; + +describe('IBM Resilient formatter', () => { + const theCase = { + connector: { fields: { incidentTypes: ['2'], severityCode: '2' } }, + } as CaseResponse; + + it('it formats correctly', async () => { + const res = await resilientExternalServiceFormatter.format(theCase, []); + expect(res).toEqual({ ...theCase.connector.fields }); + }); + + it('it formats correctly when fields do not exist ', async () => { + const invalidFields = { tags: ['a tag'], connector: { fields: null } } as CaseResponse; + const res = await resilientExternalServiceFormatter.format(invalidFields, []); + expect(res).toEqual({ incidentTypes: null, severityCode: null }); + }); +}); diff --git a/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.ts b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.ts new file mode 100644 index 0000000000000..76554dce32797 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ResilientFieldsType, ConnectorResillientTypeFields } from '../../../common/api'; +import { ExternalServiceFormatter } from '../types'; + +const format: ExternalServiceFormatter['format'] = (theCase) => { + const { incidentTypes = null, severityCode = null } = + (theCase.connector.fields as ConnectorResillientTypeFields['fields']) ?? {}; + return { incidentTypes, severityCode }; +}; + +export const resilientExternalServiceFormatter: ExternalServiceFormatter = { + format, +}; diff --git a/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts b/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts new file mode 100644 index 0000000000000..60faa82a9e3fa --- /dev/null +++ b/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ServiceNowITSMFieldsType, ConnectorServiceNowITSMTypeFields } from '../../../common/api'; +import { ExternalServiceFormatter } from '../types'; + +const format: ExternalServiceFormatter['format'] = (theCase) => { + const { severity = null, urgency = null, impact = null } = + (theCase.connector.fields as ConnectorServiceNowITSMTypeFields['fields']) ?? {}; + return { severity, urgency, impact }; +}; + +export const serviceNowITSMExternalServiceFormatter: ExternalServiceFormatter = { + format, +}; diff --git a/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts b/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts new file mode 100644 index 0000000000000..033f184c7e751 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseResponse } from '../../../common/api'; +import { serviceNowITSMExternalServiceFormatter } from './itsm_formatter'; + +describe('ITSM formatter', () => { + const theCase = { + connector: { fields: { severity: '2', urgency: '2', impact: '2' } }, + } as CaseResponse; + + it('it formats correctly', async () => { + const res = await serviceNowITSMExternalServiceFormatter.format(theCase, []); + expect(res).toEqual(theCase.connector.fields); + }); + + it('it formats correctly when fields do not exist ', async () => { + const invalidFields = { connector: { fields: null } } as CaseResponse; + const res = await serviceNowITSMExternalServiceFormatter.format(invalidFields, []); + expect(res).toEqual({ severity: null, urgency: null, impact: null }); + }); +}); diff --git a/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts new file mode 100644 index 0000000000000..4faca62c6e706 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseResponse } from '../../../common/api'; +import { serviceNowSIRExternalServiceFormatter } from './sir_formatter'; + +describe('ITSM formatter', () => { + const theCase = { + connector: { + fields: { + destIp: true, + sourceIp: true, + category: 'Denial of Service', + subcategory: 'Inbound DDos', + malwareHash: true, + malwareUrl: true, + priority: '2 - High', + }, + }, + } as CaseResponse; + + it('it formats correctly without alerts', async () => { + const res = await serviceNowSIRExternalServiceFormatter.format(theCase, []); + expect(res).toEqual({ + dest_ip: null, + source_ip: null, + category: 'Denial of Service', + subcategory: 'Inbound DDos', + malware_hash: null, + malware_url: null, + priority: '2 - High', + }); + }); + + it('it formats correctly when fields do not exist ', async () => { + const invalidFields = { connector: { fields: null } } as CaseResponse; + const res = await serviceNowSIRExternalServiceFormatter.format(invalidFields, []); + expect(res).toEqual({ + dest_ip: null, + source_ip: null, + category: null, + subcategory: null, + malware_hash: null, + malware_url: null, + priority: null, + }); + }); + + it('it formats correctly with alerts', async () => { + const alerts = [ + { + id: 'alert-1', + index: 'index-1', + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, + }, + url: { full: 'https://attack.com' }, + }, + { + id: 'alert-2', + index: 'index-2', + destination: { ip: '192.168.1.4' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, + }, + url: { full: 'https://attack.com/api' }, + }, + ]; + const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts); + expect(res).toEqual({ + dest_ip: '192.168.1.1,192.168.1.4', + source_ip: '192.168.1.2,192.168.1.3', + category: 'Denial of Service', + subcategory: 'Inbound DDos', + malware_hash: + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08,60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', + malware_url: 'https://attack.com,https://attack.com/api', + priority: '2 - High', + }); + }); + + it('it handles duplicates correctly', async () => { + const alerts = [ + { + id: 'alert-1', + index: 'index-1', + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, + }, + url: { full: 'https://attack.com' }, + }, + { + id: 'alert-2', + index: 'index-2', + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, + }, + url: { full: 'https://attack.com/api' }, + }, + ]; + const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts); + expect(res).toEqual({ + dest_ip: '192.168.1.1', + source_ip: '192.168.1.2,192.168.1.3', + category: 'Denial of Service', + subcategory: 'Inbound DDos', + malware_hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + malware_url: 'https://attack.com,https://attack.com/api', + priority: '2 - High', + }); + }); + + it('it formats correctly when field is not selected', async () => { + const alerts = [ + { + id: 'alert-1', + index: 'index-1', + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, + }, + url: { full: 'https://attack.com' }, + }, + { + id: 'alert-2', + index: 'index-2', + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, + }, + url: { full: 'https://attack.com/api' }, + }, + ]; + + const newCase = { + ...theCase, + connector: { fields: { ...theCase.connector.fields, destIp: false, malwareHash: false } }, + } as CaseResponse; + + const res = await serviceNowSIRExternalServiceFormatter.format(newCase, alerts); + expect(res).toEqual({ + dest_ip: null, + source_ip: '192.168.1.2,192.168.1.3', + category: 'Denial of Service', + subcategory: 'Inbound DDos', + malware_hash: null, + malware_url: 'https://attack.com,https://attack.com/api', + priority: '2 - High', + }); + }); +}); diff --git a/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts new file mode 100644 index 0000000000000..d2458e6c7ae53 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { get } from 'lodash/fp'; +import { ConnectorServiceNowSIRTypeFields } from '../../../common/api'; +import { ExternalServiceFormatter } from '../types'; +interface ExternalServiceParams { + dest_ip: string | null; + source_ip: string | null; + category: string | null; + subcategory: string | null; + malware_hash: string | null; + malware_url: string | null; + priority: string | null; +} +type SirFieldKey = 'dest_ip' | 'source_ip' | 'malware_hash' | 'malware_url'; +type AlertFieldMappingAndValues = Record< + string, + { alertPath: string; sirFieldKey: SirFieldKey; add: boolean } +>; +const format: ExternalServiceFormatter['format'] = (theCase, alerts) => { + const { + destIp = null, + sourceIp = null, + category = null, + subcategory = null, + malwareHash = null, + malwareUrl = null, + priority = null, + } = (theCase.connector.fields as ConnectorServiceNowSIRTypeFields['fields']) ?? {}; + const alertFieldMapping: AlertFieldMappingAndValues = { + destIp: { alertPath: 'destination.ip', sirFieldKey: 'dest_ip', add: !!destIp }, + sourceIp: { alertPath: 'source.ip', sirFieldKey: 'source_ip', add: !!sourceIp }, + malwareHash: { alertPath: 'file.hash.sha256', sirFieldKey: 'malware_hash', add: !!malwareHash }, + malwareUrl: { alertPath: 'url.full', sirFieldKey: 'malware_url', add: !!malwareUrl }, + }; + + const manageDuplicate: Record> = { + dest_ip: new Set(), + source_ip: new Set(), + malware_hash: new Set(), + malware_url: new Set(), + }; + + let sirFields: Record = { + dest_ip: null, + source_ip: null, + malware_hash: null, + malware_url: null, + }; + + const fieldsToAdd = (Object.keys(alertFieldMapping) as SirFieldKey[]).filter( + (key) => alertFieldMapping[key].add + ); + + if (fieldsToAdd.length > 0) { + sirFields = alerts.reduce>((acc, alert) => { + fieldsToAdd.forEach((alertField) => { + const field = get(alertFieldMapping[alertField].alertPath, alert); + if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { + manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); + acc = { + ...acc, + [alertFieldMapping[alertField].sirFieldKey]: `${ + acc[alertFieldMapping[alertField].sirFieldKey] != null + ? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}` + : field + }`, + }; + } + }); + return acc; + }, sirFields); + } + + return { + ...sirFields, + category, + subcategory, + priority, + }; +}; +export const serviceNowSIRExternalServiceFormatter: ExternalServiceFormatter = { + format, +}; diff --git a/x-pack/plugins/case/server/connectors/types.ts b/x-pack/plugins/case/server/connectors/types.ts new file mode 100644 index 0000000000000..8e7eb91ad2dc6 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/types.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from 'kibana/server'; +import { + ActionTypeConfig, + ActionTypeSecrets, + ActionTypeParams, + ActionType, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../actions/server/types'; +import { CaseResponse, ConnectorTypes } from '../../common/api'; +import { CaseClientGetAlertsResponse } from '../client/alerts/types'; +import { + CaseServiceSetup, + CaseConfigureServiceSetup, + CaseUserActionServiceSetup, + ConnectorMappingsServiceSetup, + AlertServiceContract, +} from '../services'; + +export interface GetActionTypeParams { + logger: Logger; + caseService: CaseServiceSetup; + caseConfigureService: CaseConfigureServiceSetup; + connectorMappingsService: ConnectorMappingsServiceSetup; + userActionService: CaseUserActionServiceSetup; + alertsService: AlertServiceContract; +} + +export interface RegisterConnectorsArgs extends GetActionTypeParams { + actionsRegisterType< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void + >( + actionType: ActionType + ): void; +} + +export type FormatterConnectorTypes = Exclude; + +export interface ExternalServiceFormatter { + format: (theCase: CaseResponse, alerts: CaseClientGetAlertsResponse) => TExternalServiceParams; +} + +export type ExternalServiceFormatterMapper = { + [x in FormatterConnectorTypes]: ExternalServiceFormatter; +}; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 8b4fdc73dab44..5d05db165f637 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; +import { + IContextProvider, + KibanaRequest, + KibanaResponseFactory, + Logger, + PluginInitializerContext, +} from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -123,11 +129,13 @@ export class CasePlugin { const getCaseClientWithRequestAndContext = async ( context: CasesRequestHandlerContext, - request: KibanaRequest + request: KibanaRequest, + response: KibanaResponseFactory ) => { return createCaseClient({ savedObjectsClient: core.savedObjects.getScopedClient(request), request, + response, caseService: this.caseService!, caseConfigureService: this.caseConfigureService!, connectorMappingsService: this.connectorMappingsService!, @@ -161,7 +169,7 @@ export class CasePlugin { userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; }): IContextProvider => { - return async (context, request) => { + return async (context, request, response) => { const [{ savedObjects }] = await core.getStartServices(); return { getCaseClient: () => { @@ -172,8 +180,9 @@ export class CasePlugin { connectorMappingsService, userActionService, alertsService, - request, context, + request, + response, }); }, }; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts index 8dc970d235fea..18730effdf55a 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -17,6 +17,7 @@ import { CASE_SAVED_OBJECT, CASE_CONFIGURE_SAVED_OBJECT, CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, } from '../../../saved_object_types'; export const createMockSavedObjectsRepository = ({ @@ -24,11 +25,13 @@ export const createMockSavedObjectsRepository = ({ caseCommentSavedObject = [], caseConfigureSavedObject = [], caseMappingsSavedObject = [], + caseUserActionsSavedObject = [], }: { caseSavedObject?: any[]; caseCommentSavedObject?: any[]; caseConfigureSavedObject?: any[]; caseMappingsSavedObject?: any[]; + caseUserActionsSavedObject?: any[]; } = {}) => { const mockSavedObjectsClientContract = ({ bulkGet: jest.fn((objects: SavedObjectsBulkGetObject[]) => { @@ -57,6 +60,7 @@ export const createMockSavedObjectsRepository = ({ }), }; }), + bulkCreate: jest.fn(), bulkUpdate: jest.fn((objects: Array>) => { return { saved_objects: objects.map(({ id, type, attributes }) => { @@ -136,6 +140,16 @@ export const createMockSavedObjectsRepository = ({ saved_objects: caseCommentSavedObject, }; } + + if (findArgs.type === CASE_USER_ACTION_SAVED_OBJECT) { + return { + page: 1, + per_page: 5, + total: caseUserActionsSavedObject.length, + saved_objects: caseUserActionsSavedObject, + }; + } + return { page: 1, per_page: 5, diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts index 5e2c29f29a3e7..1abd44aec1552 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts @@ -10,3 +10,4 @@ export { createMockSavedObjectsRepository } from './create_mock_so_repository'; export { createRouteContext } from './route_contexts'; export { authenticationMock } from './authc_mock'; export { createRoute } from './mock_router'; +export { createActionsClient } from './mock_actions_client'; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts new file mode 100644 index 0000000000000..d153c328cbb91 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsErrorHelpers } from 'src/core/server'; +import { actionsClientMock } from '../../../../../actions/server/mocks'; +import { + getActions, + getActionTypes, + getActionExecuteResults, +} from '../__mocks__/request_responses'; + +export const createActionsClient = () => { + const actionsMock = actionsClientMock.create(); + actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); + actionsMock.listTypes.mockImplementation(() => Promise.resolve(getActionTypes())); + actionsMock.get.mockImplementation(({ id }) => { + const actions = getActions(); + const action = actions.find((a) => a.id === id); + if (action) { + return Promise.resolve(action); + } else { + return Promise.reject(SavedObjectsErrorHelpers.createGenericNotFoundError('action', id)); + } + }); + actionsMock.execute.mockImplementation(({ actionId }) => + Promise.resolve(getActionExecuteResults(actionId)) + ); + + return actionsMock; +}; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 4ac5004eb3dfd..514f77a8f953d 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -8,6 +8,7 @@ import { SavedObject } from 'kibana/server'; import { CaseStatuses, + CaseUserActionAttributes, CommentAttributes, CommentType, ConnectorMappings, @@ -15,7 +16,10 @@ import { ESCaseAttributes, ESCasesConfigureAttributes, } from '../../../../common/api'; -import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../saved_object_types'; +import { + CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, +} from '../../../saved_object_types'; import { mappings } from '../../../client/configure/mock'; export const mockCases: Array> = [ @@ -424,3 +428,44 @@ export const mockCaseMappings: Array> = [ references: [], }, ]; + +export const mockUserActions: Array> = [ + { + type: CASE_USER_ACTION_SAVED_OBJECT, + id: 'mock-user-actions-1', + attributes: { + action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'], + action: 'create', + action_at: '2021-02-03T17:41:03.771Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + '{"title":"A case","tags":["case"],"description":"Yeah!","connector":{"id":"connector-od","name":"My Connector","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', + old_value: null, + }, + version: 'WzYsMV0=', + references: [], + }, + { + type: CASE_USER_ACTION_SAVED_OBJECT, + id: 'mock-user-actions-2', + attributes: { + action_field: ['comment'], + action: 'create', + action_at: '2021-02-03T17:44:21.067Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + '{"type":"alert","alertId":"cec3da90fb37a44407145adf1593f3b0d5ad94c4654201f773d63b5d4706128e","index":".siem-signals-default-000008"}', + old_value: null, + }, + version: 'WzYsMV0=', + references: [], + }, +]; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index 9f7258fc7edaf..74665ffdc5b16 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -5,24 +5,25 @@ * 2.0. */ -import { KibanaRequest } from 'src/core/server'; -import { loggingSystemMock, elasticsearchServiceMock } from 'src/core/server/mocks'; -import { actionsClientMock } from '../../../../../actions/server/mocks'; +import { KibanaRequest, kibanaResponseFactory } from '../../../../../../../src/core/server'; +import { + loggingSystemMock, + elasticsearchServiceMock, +} from '../../../../../../../src/core/server/mocks'; import { createCaseClient } from '../../../client'; import { AlertService, CaseService, CaseConfigureService, ConnectorMappingsService, + CaseUserActionService, } from '../../../services'; -import { getActions, getActionTypes } from '../__mocks__/request_responses'; import { authenticationMock } from '../__fixtures__'; import type { CasesRequestHandlerContext } from '../../../types'; +import { createActionsClient } from './mock_actions_client'; export const createRouteContext = async (client: any, badAuth = false) => { - const actionsMock = actionsClientMock.create(); - actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); - actionsMock.listTypes.mockImplementation(() => Promise.resolve(getActionTypes())); + const actionsMock = createActionsClient(); const log = loggingSystemMock.create().get('case'); const esClientMock = elasticsearchServiceMock.createClusterClient(); @@ -30,11 +31,13 @@ export const createRouteContext = async (client: any, badAuth = false) => { const caseServicePlugin = new CaseService(log); const caseConfigureServicePlugin = new CaseConfigureService(log); const connectorMappingsServicePlugin = new ConnectorMappingsService(log); + const caseUserActionsServicePlugin = new CaseUserActionService(log); const caseService = await caseServicePlugin.setup({ authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), }); const caseConfigureService = await caseConfigureServicePlugin.setup(); + const userActionService = await caseUserActionsServicePlugin.setup(); const alertsService = new AlertService(); alertsService.initialize(esClientMock); @@ -59,16 +62,14 @@ export const createRouteContext = async (client: any, badAuth = false) => { const caseClient = createCaseClient({ savedObjectsClient: client, request: {} as KibanaRequest, + response: kibanaResponseFactory, caseService, caseConfigureService, connectorMappingsService, - userActionService: { - postUserActions: jest.fn(), - getUserActions: jest.fn(), - }, + userActionService, alertsService, context, }); - return context; + return { context, services: { userActionService } }; }; diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index f2109167527c7..ae14b44e7dffe 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -10,11 +10,9 @@ import { CasePostRequest, CasesConfigureRequest, ConnectorTypes, - PostPushRequest, } from '../../../../common/api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../../actions/server/types'; -import { params } from '../cases/configure/mock'; export const newCase: CasePostRequest = { title: 'My new case', @@ -74,6 +72,16 @@ export const getActions = (): FindActionResult[] => [ isPreconfigured: false, referencedByCount: 0, }, + { + id: 'for-mock-case-id-3', + actionTypeId: '.jira', + name: 'For mock case id 3', + config: { + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, ]; export const getActionTypes = (): ActionTypeConnector[] => [ @@ -119,6 +127,18 @@ export const getActionTypes = (): ActionTypeConnector[] => [ }, ]; +export const getActionExecuteResults = (actionId = '123') => ({ + status: 'ok' as const, + data: { + title: 'RJ2-200', + id: '10663', + pushedDate: '2020-12-17T00:32:40.738Z', + url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', + comments: [], + }, + actionId, +}); + export const newConfiguration: CasesConfigureRequest = { connector: { id: '456', @@ -129,11 +149,6 @@ export const newConfiguration: CasesConfigureRequest = { closure_type: 'close-by-pushing', }; -export const newPostPushRequest: PostPushRequest = { - params: params[ConnectorTypes.jira], - connector_type: ConnectorTypes.jira, -}; - export const executePushResponse = { status: 'ok', data: { diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts index 9454f582e50c6..dcbcd7b9e246d 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts @@ -33,14 +33,14 @@ describe('DELETE comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(204); }); it(`returns an error when thrown from deleteComment service`, async () => { @@ -53,14 +53,14 @@ describe('DELETE comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts index a1f4b8c2583cf..8ee43eaba8a82 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts @@ -34,14 +34,14 @@ describe('GET comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); const myPayload = mockCaseComments.find((s) => s.id === 'mock-comment-1'); expect(myPayload).not.toBeUndefined(); @@ -59,13 +59,13 @@ describe('GET comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts index 3bd8a688e1bba..33dc24d776c70 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts @@ -41,14 +41,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1].comment).toEqual( 'Update my comment' @@ -71,14 +71,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1].alertId).toEqual( 'new-id' @@ -102,14 +102,14 @@ describe('PATCH comment', () => { body: requestAttributes, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -130,14 +130,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -161,14 +161,14 @@ describe('PATCH comment', () => { body: requestAttributes, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -190,14 +190,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -219,14 +219,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); expect(response.payload.message).toEqual('You cannot change the type of the comment.'); @@ -247,14 +247,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(409); }); @@ -273,14 +273,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index 54699415cd984..0ab038a62ac77 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -43,14 +43,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual( 'mock-comment' @@ -71,14 +71,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual( 'mock-comment' @@ -95,14 +95,14 @@ describe('POST comment', () => { body: {}, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); @@ -124,14 +124,14 @@ describe('POST comment', () => { body: requestAttributes, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -152,14 +152,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -183,14 +183,14 @@ describe('POST comment', () => { body: requestAttributes, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -212,14 +212,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -238,14 +238,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); }); @@ -262,14 +262,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); @@ -289,7 +289,7 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -297,7 +297,7 @@ describe('POST comment', () => { true ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1]).toEqual({ comment: 'Wow, good luck catching that bad meanie!', diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts index ddcbb3522f986..ff4216a05ae58 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -34,7 +34,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -57,7 +57,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], version: undefined }], caseMappingsSavedObject: mockCaseMappings, @@ -98,7 +98,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [], }) @@ -116,7 +116,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], }) @@ -133,7 +133,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: [], diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 0f74b7291dd81..17972e129a825 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -33,9 +33,9 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } const caseClient = context.case.getCaseClient(); - const actionsClient = await context.actions?.getActionsClient(); + const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); + throw Boom.notFound('Action client not found'); } try { mappings = await caseClient.getMappings({ diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts index 1e37918d7766a..3fa0fe2f83f79 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -32,7 +32,7 @@ describe('GET connectors', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -54,7 +54,7 @@ describe('GET connectors', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -106,6 +106,16 @@ describe('GET connectors', () => { isPreconfigured: false, referencedByCount: 0, }, + { + id: 'for-mock-case-id-3', + actionTypeId: '.jira', + name: 'For mock case id 3', + config: { + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, ]); }); @@ -115,7 +125,7 @@ describe('GET connectors', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index fb0595f858d4e..0a368e0276bb5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -14,18 +14,15 @@ import { FindActionResult } from '../../../../../../actions/server/types'; import { CASE_CONFIGURE_CONNECTORS_URL, - SERVICENOW_ACTION_TYPE_ID, - JIRA_ACTION_TYPE_ID, - RESILIENT_ACTION_TYPE_ID, + SUPPORTED_CONNECTORS, } from '../../../../../common/constants'; const isConnectorSupported = ( action: FindActionResult, actionTypes: Record ): boolean => - [SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( - action.actionTypeId - ) && actionTypes[action.actionTypeId]?.enabledInLicense; + SUPPORTED_CONNECTORS.includes(action.actionTypeId) && + actionTypes[action.actionTypeId]?.enabledInLicense; /* * Be aware that this api will only return 20 connectors @@ -39,10 +36,10 @@ export function initCaseConfigureGetActionConnector({ router }: RouteDeps) { }, async (context, request, response) => { try { - const actionsClient = await context.actions?.getActionsClient(); + const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); + throw Boom.notFound('Action client not found'); } const actionTypes = (await actionsClient.listTypes()).reduce( diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts b/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts deleted file mode 100644 index 9959a3e4acee6..0000000000000 --- a/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts +++ /dev/null @@ -1,76 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - ServiceConnectorCaseParams, - ServiceConnectorCommentParams, - ConnectorMappingsAttributes, - ConnectorTypes, -} from '../../../../../common/api/connectors'; -export const updateUser = { - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'another' }, -}; -const entity = { - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, -}; -export const comment: ServiceConnectorCommentParams = { - comment: 'first comment', - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - ...entity, -}; -export const defaultPipes = ['informationCreated']; -const basicParams = { - comments: [comment], - description: 'a description', - title: 'a title', - savedObjectId: '1231231231232', - externalId: null, -}; -export const params = { - [ConnectorTypes.jira]: { - ...basicParams, - issueType: '10003', - priority: 'Highest', - parent: '5002', - ...entity, - } as ServiceConnectorCaseParams, - [ConnectorTypes.resilient]: { - ...basicParams, - incidentTypes: ['10003'], - severityCode: '1', - ...entity, - } as ServiceConnectorCaseParams, - [ConnectorTypes.servicenow]: { - ...basicParams, - impact: '3', - severity: '1', - urgency: '2', - ...entity, - } as ServiceConnectorCaseParams, - [ConnectorTypes.none]: {}, -}; -export const mappings: ConnectorMappingsAttributes[] = [ - { - source: 'title', - target: 'short_description', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'append', - }, - { - source: 'comments', - target: 'comments', - action_type: 'append', - }, -]; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts index c67a1c064a82f..f43f561e30e10 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts @@ -42,7 +42,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -76,7 +76,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -115,7 +115,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -153,7 +153,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: [], @@ -193,7 +193,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [], }) @@ -215,7 +215,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -243,7 +243,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index f847c4f776bf0..6925f116136b3 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -66,7 +66,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } const caseClient = context.case.getCaseClient(); - const actionsClient = await context.actions?.getActionsClient(); + const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { throw Boom.notFound('Action client have not been found'); } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts index 0a7f3ef488fce..7dcb7d1fa12ca 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts @@ -40,7 +40,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -73,7 +73,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: [], @@ -113,7 +113,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -154,7 +154,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -180,7 +180,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -206,7 +206,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -232,7 +232,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -258,7 +258,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -282,7 +282,7 @@ describe('POST configuration', () => { caseMappingsSavedObject: mockCaseMappings, }); - const context = await createRouteContext(savedObjectRepository); + const { context } = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -302,7 +302,7 @@ describe('POST configuration', () => { caseMappingsSavedObject: mockCaseMappings, }); - const context = await createRouteContext(savedObjectRepository); + const { context } = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -325,7 +325,7 @@ describe('POST configuration', () => { caseMappingsSavedObject: mockCaseMappings, }); - const context = await createRouteContext(savedObjectRepository); + const { context } = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -341,7 +341,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], }) @@ -359,7 +359,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-delete' }], }) @@ -384,7 +384,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -411,7 +411,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -437,7 +437,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -459,7 +459,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index 8e5fd95facc3d..0bcf2ac18740f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -39,9 +39,9 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } const caseClient = context.case.getCaseClient(); - const actionsClient = await context.actions?.getActionsClient(); + const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); + throw Boom.notFound('Action client not found'); } const client = context.core.savedObjects.client; const query = pipe( diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts deleted file mode 100644 index e382813dbf0c5..0000000000000 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts +++ /dev/null @@ -1,106 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCaseMappings, -} from '../../__fixtures__'; - -import { initPostPushToService } from './post_push_to_service'; -import { executePushResponse, newPostPushRequest } from '../../__mocks__/request_responses'; -import { CASE_CONFIGURE_PUSH_URL } from '../../../../../common/constants'; -import type { CasesRequestHandlerContext } from '../../../../types'; - -describe('Post push to service', () => { - let routeHandler: RequestHandler; - const req = httpServerMock.createKibanaRequest({ - path: `${CASE_CONFIGURE_PUSH_URL}`, - method: 'post', - params: { - connector_id: '666', - }, - body: newPostPushRequest, - }); - let context: CasesRequestHandlerContext; - beforeAll(async () => { - routeHandler = await createRoute(initPostPushToService, 'post'); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2020-04-09T09:43:51.778Z'), - })); - context = await createRouteContext( - createMockSavedObjectsRepository({ - caseMappingsSavedObject: mockCaseMappings, - }) - ); - }); - - it('Happy path - posts success', async () => { - const betterContext = ({ - ...context, - actions: { - ...context.actions, - getActionsClient: () => { - const actions = context!.actions!.getActionsClient(); - return { - ...actions, - execute: jest.fn().mockImplementation(({ actionId }) => { - return { - status: 'ok', - data: { - title: 'RJ2-200', - id: '10663', - pushedDate: '2020-12-17T00:32:40.738Z', - url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', - comments: [], - }, - actionId, - }; - }), - }; - }, - }, - } as unknown) as CasesRequestHandlerContext; - - const res = await routeHandler(betterContext, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(res.payload).toEqual({ - ...executePushResponse, - actionId: '666', - }); - }); - it('Unhappy path - context case missing', async () => { - const betterContext = ({ - ...context, - case: null, - } as unknown) as CasesRequestHandlerContext; - - const res = await routeHandler(betterContext, req, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload.isBoom).toBeTruthy(); - expect(res.payload.output.payload.message).toEqual( - 'RouteHandlerContext is not registered for cases' - ); - }); - it('Unhappy path - context actions missing', async () => { - const betterContext = ({ - ...context, - actions: null, - } as unknown) as CasesRequestHandlerContext; - - const res = await routeHandler(betterContext, req, kibanaResponseFactory); - expect(res.status).toEqual(404); - expect(res.payload.isBoom).toBeTruthy(); - expect(res.payload.output.payload.message).toEqual('Action client have not been found'); - }); -}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts deleted file mode 100644 index b8ba1a9ccb6ef..0000000000000 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts +++ /dev/null @@ -1,81 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import Boom from '@hapi/boom'; -import { RouteDeps } from '../../types'; -import { escapeHatch, wrapError } from '../../utils'; - -import { CASE_CONFIGURE_PUSH_URL } from '../../../../../common/constants'; -import { - ConnectorRequestParamsRt, - PostPushRequestRt, - throwErrors, -} from '../../../../../common/api'; -import { mapIncident } from './utils'; - -export function initPostPushToService({ router }: RouteDeps) { - router.post( - { - path: CASE_CONFIGURE_PUSH_URL, - validate: { - params: escapeHatch, - body: escapeHatch, - }, - }, - async (context, request, response) => { - try { - if (!context.case) { - throw Boom.badRequest('RouteHandlerContext is not registered for cases'); - } - const caseClient = context.case.getCaseClient(); - const actionsClient = await context.actions?.getActionsClient(); - if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); - } - const params = pipe( - ConnectorRequestParamsRt.decode(request.params), - fold(throwErrors(Boom.badRequest), identity) - ); - const body = pipe( - PostPushRequestRt.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const myConnectorMappings = await caseClient.getMappings({ - actionsClient, - caseClient, - connectorId: params.connector_id, - connectorType: body.connector_type, - }); - - const res = await mapIncident( - actionsClient, - params.connector_id, - body.connector_type, - myConnectorMappings, - body.params - ); - const pushRes = await actionsClient.execute({ - actionId: params.connector_id, - params: { - subAction: 'pushToService', - subActionParams: res, - }, - }); - - return response.ok({ - body: pushRes, - }); - } catch (error) { - return response.customError(wrapError(error)); - } - } - ); -} diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts index 84e452ea8e871..d588950bec9aa 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts @@ -33,14 +33,14 @@ describe('DELETE case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(204); }); it(`returns an error when thrown from deleteCase service`, async () => { @@ -52,14 +52,14 @@ describe('DELETE case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); }); it(`returns an error when thrown from getAllCaseComments service`, async () => { @@ -71,14 +71,14 @@ describe('DELETE case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); }); it(`returns an error when thrown from deleteComment service`, async () => { @@ -90,14 +90,14 @@ describe('DELETE case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, caseCommentSavedObject: mockCasesErrorTriggerData, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts index acd7de1e8643e..ca9f731ca5010 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts @@ -30,13 +30,13 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.cases).toHaveLength(4); // mockSavedObjectsRepository do not support filters and returns all cases every time. @@ -51,13 +51,13 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.cases[2].connector.id).toEqual('123'); }); @@ -68,13 +68,13 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.cases[0].connector.id).toEqual('none'); }); @@ -85,14 +85,14 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.cases[0].connector.id).toEqual('none'); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts index 7aa6f110a0079..968dd0424fe3f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts @@ -40,13 +40,13 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); const savedObject = (mockCases.find( (s) => s.id === 'mock-id-1' ) as unknown) as SavedObject; @@ -71,13 +71,13 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); @@ -95,14 +95,14 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments).toHaveLength(5); @@ -120,13 +120,13 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); }); @@ -143,13 +143,13 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.connector).toEqual({ @@ -172,14 +172,14 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.connector).toEqual({ @@ -202,14 +202,14 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.connector).toEqual({ 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 f563fc274b18b..55377d93e528d 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 @@ -7,9 +7,8 @@ import { schema } from '@kbn/config-schema'; -import { CaseResponseRt } from '../../../../common/api'; import { RouteDeps } from '../types'; -import { flattenCaseSavedObject, wrapError } from '../utils'; +import { wrapError } from '../utils'; import { CASE_DETAILS_URL } from '../../../../common/constants'; export function initGetCaseApi({ caseConfigureService, caseService, router }: RouteDeps) { @@ -26,44 +25,17 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro }, }, async (context, request, response) => { - try { - const client = context.core.savedObjects.client; - const includeComments = JSON.parse(request.query.includeComments); - - const [theCase] = await Promise.all([ - caseService.getCase({ - client, - caseId: request.params.case_id, - }), - ]); - - if (!includeComments) { - return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: theCase, - }) - ), - }); - } + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const theComments = await caseService.getAllCaseComments({ - client, - caseId: request.params.case_id, - options: { - sortField: 'created_at', - sortOrder: 'asc', - }, - }); + const caseClient = context.case.getCaseClient(); + const includeComments = JSON.parse(request.query.includeComments); + const id = request.params.case_id; + try { return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: theCase, - comments: theComments.saved_objects, - totalComment: theComments.total, - }) - ), + body: await caseClient.get({ id, includeComments }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts index 95f7e5bb19a01..6d1134b15b65e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts @@ -44,13 +44,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload).toEqual([ { @@ -97,14 +97,14 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload).toEqual([ { @@ -151,13 +151,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload).toEqual([ { @@ -204,13 +204,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload[0].connector.id).toEqual('none'); }); @@ -230,13 +230,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload[0].connector.id).toEqual('123'); }); @@ -261,13 +261,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload[0].connector).toEqual({ id: '456', @@ -292,13 +292,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(409); }); @@ -317,14 +317,14 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(406); }); @@ -343,13 +343,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts index 997516d2e30b6..292e2c6775a80 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts @@ -49,13 +49,13 @@ describe('POST cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.id).toEqual('mock-it'); expect(response.payload.status).toEqual('open'); @@ -88,14 +88,14 @@ describe('POST cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.connector).toEqual({ id: '123', @@ -121,13 +121,13 @@ describe('POST cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); }); @@ -146,13 +146,13 @@ describe('POST cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); @@ -179,7 +179,7 @@ describe('POST cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, @@ -187,7 +187,7 @@ describe('POST cases', () => { true ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload).toEqual({ closed_at: null, diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts index 549195966b2a7..49801ea4e2f3e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts @@ -13,63 +13,187 @@ import { createRoute, createRouteContext, mockCases, + mockCaseConfigure, + mockCaseMappings, + mockUserActions, + mockCaseComments, } from '../__fixtures__'; -import { initPushCaseUserActionApi } from './push_case'; -import { CASE_DETAILS_URL } from '../../../../common/constants'; -import { mockCaseConfigure } from '../__fixtures__/mock_saved_objects'; +import { initPushCaseApi } from './push_case'; +import { CasesRequestHandlerContext } from '../../../types'; +import { getCasePushUrl } from '../../../../common/api/helpers'; describe('Push case', () => { let routeHandler: RequestHandler; const mockDate = '2019-11-25T21:54:48.952Z'; - const caseExternalServiceRequestBody = { - connector_id: 'connector_id', - connector_name: 'connector_name', - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', - }; + const caseId = 'mock-id-3'; + const connectorId = '123'; + const path = getCasePushUrl(caseId, connectorId); + beforeAll(async () => { - routeHandler = await createRoute(initPushCaseUserActionApi, 'post'); + routeHandler = await createRoute(initPushCaseApi, 'post'); const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; spyOnDate.mockImplementation(() => ({ toISOString: jest.fn().mockReturnValue(mockDate), })); }); + it(`Pushes a case`, async () => { const request = httpServerMock.createKibanaRequest({ - path: `${CASE_DETAILS_URL}/_push`, + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const response = await routeHandler(context, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.external_service).toEqual({ + connector_id: connectorId, + connector_name: 'ServiceNow', + external_id: '10663', + external_title: 'RJ2-200', + external_url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', + pushed_at: mockDate, + pushed_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + }); + }); + + it(`Pushes a case with comments`, async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + caseCommentSavedObject: [mockCaseComments[0]], + }) + ); + + const response = await routeHandler(context, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.comments[0].pushed_at).toEqual(mockDate); + expect(response.payload.comments[0].pushed_by).toEqual({ + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }); + }); + + it(`Filters comments with type alert correctly`, async () => { + const request = httpServerMock.createKibanaRequest({ + path, method: 'post', params: { - case_id: 'mock-id-3', + case_id: caseId, + connector_id: connectorId, }, - body: caseExternalServiceRequestBody, + body: {}, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + caseCommentSavedObject: [mockCaseComments[0], mockCaseComments[3]], }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const caseClient = context.case.getCaseClient(); + caseClient.getAlerts = jest.fn().mockResolvedValue([]); + + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.external_service.pushed_at).toEqual(mockDate); - expect(response.payload.external_service.connector_id).toEqual('connector_id'); - expect(response.payload.closed_at).toEqual(null); + expect(caseClient.getAlerts).toHaveBeenCalledWith({ ids: ['test-id'] }); + }); + + it(`Calls execute with correct arguments`, async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: 'for-mock-case-id-3', + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const actionsClient = context.actions.getActionsClient(); + + await routeHandler(context, request, kibanaResponseFactory); + expect(actionsClient.execute).toHaveBeenCalledWith({ + actionId: 'for-mock-case-id-3', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + issueType: 'Task', + parent: null, + priority: 'High', + labels: ['LOLBins'], + summary: 'Another bad one (created at 2019-11-25T22:32:17.947Z by elastic)', + description: + 'Oh no, a bad meanie going LOLBins all over the place! (created at 2019-11-25T22:32:17.947Z by elastic)', + externalId: null, + }, + comments: [], + }, + }, + }); }); + it(`Pushes a case and closes when closure_type: 'close-by-pushing'`, async () => { const request = httpServerMock.createKibanaRequest({ - path: `${CASE_DETAILS_URL}/_push`, + path, method: 'post', params: { - case_id: 'mock-id-3', + case_id: caseId, + connector_id: connectorId, }, - body: caseExternalServiceRequestBody, + body: {}, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseUserActionsSavedObject: mockUserActions, caseConfigureSavedObject: [ { ...mockCaseConfigure[0], @@ -82,30 +206,259 @@ describe('Push case', () => { }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.external_service.pushed_at).toEqual(mockDate); - expect(response.payload.external_service.connector_id).toEqual('connector_id'); expect(response.payload.closed_at).toEqual(mockDate); }); - it(`Returns an error if pushCaseUserAction throws`, async () => { + it(`post the correct user action`, async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context, services } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + services.userActionService.postUserActions = jest.fn(); + const postUserActions = services.userActionService.postUserActions as jest.Mock; + + const response = await routeHandler(context, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(postUserActions.mock.calls[0][0].actions[0].attributes).toEqual({ + action: 'push-to-service', + action_at: '2019-11-25T21:54:48.952Z', + action_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + action_field: ['pushed'], + new_value: + '{"pushed_at":"2019-11-25T21:54:48.952Z","pushed_by":{"username":"awesome","full_name":"Awesome D00d","email":"d00d@awesome.com"},"connector_id":"123","connector_name":"ServiceNow","external_id":"10663","external_title":"RJ2-200","external_url":"https://siem-kibana.atlassian.net/browse/RJ2-200"}', + old_value: null, + }); + }); + + it('Unhappy path - case id is missing', async () => { const request = httpServerMock.createKibanaRequest({ - path: `${CASE_DETAILS_URL}/_push`, + path, method: 'post', - body: { - notagoodbody: 'Throw an error', + params: { + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(400); + }); + + it('Unhappy path - connector id is missing', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, }, + body: {}, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(400); + }); + + it('Unhappy path - case does not exists', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: 'not-exist', + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(404); + }); + + it('Unhappy path - connector does not exists', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: 'not-exists', + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(404); + }); + + it('Unhappy path - cannot push to a closed case', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: 'mock-id-4', + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(409); + expect(res.payload.output.payload.message).toBe( + 'This case Another bad one is closed. You can not pushed if the case is closed.' + ); + }); + + it('Unhappy path - throws when external service returns an error', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const actionsClient = context.actions.getActionsClient(); + (actionsClient.execute as jest.Mock).mockResolvedValue({ + status: 'error', + }); + + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(424); + expect(res.payload.output.payload.message).toBe('Error pushing to service'); + }); + + it('Unhappy path - context case missing', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const betterContext = ({ + ...context, + case: null, + } as unknown) as CasesRequestHandlerContext; + + const res = await routeHandler(betterContext, request, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload).toEqual('RouteHandlerContext is not registered for cases'); + }); + + it('Unhappy path - context actions missing', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const betterContext = ({ + ...context, + actions: null, + } as unknown) as CasesRequestHandlerContext; + + const res = await routeHandler(betterContext, request, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload).toEqual('Action client not found'); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index 218b1f16b9aab..6d670c38bbf85 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -5,204 +5,51 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; -import isEmpty from 'lodash/isEmpty'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { - flattenCaseSavedObject, - wrapError, - escapeHatch, - getCommentContextFromAttributes, -} from '../utils'; +import { wrapError, escapeHatch } from '../utils'; -import { - CaseExternalServiceRequestRt, - CaseResponseRt, - throwErrors, - CaseStatuses, -} from '../../../../common/api'; -import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; +import { throwErrors, CasePushRequestParamsRt } from '../../../../common/api'; import { RouteDeps } from '../types'; -import { CASE_DETAILS_URL } from '../../../../common/constants'; +import { CASE_PUSH_URL } from '../../../../common/constants'; -export function initPushCaseUserActionApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPushCaseApi({ router }: RouteDeps) { router.post( { - path: `${CASE_DETAILS_URL}/_push`, + path: CASE_PUSH_URL, validate: { - params: schema.object({ - case_id: schema.string(), - }), + params: escapeHatch, body: escapeHatch, }, }, async (context, request, response) => { - try { - const client = context.core.savedObjects.client; - const actionsClient = await context.actions?.getActionsClient(); - - const caseId = request.params.case_id; - const query = pipe( - CaseExternalServiceRequestRt.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request, response }); - - const pushedDate = new Date().toISOString(); - - const [myCase, myCaseConfigure, totalCommentsFindByCases, connectors] = await Promise.all([ - caseService.getCase({ - client, - caseId: request.params.case_id, - }), - caseConfigureService.find({ client }), - caseService.getAllCaseComments({ - client, - caseId, - options: { - fields: [], - page: 1, - perPage: 1, - }, - }), - actionsClient.getAll(), - ]); - - if (myCase.attributes.status === CaseStatuses.closed) { - throw Boom.conflict( - `This case ${myCase.attributes.title} is closed. You can not pushed if the case is closed.` - ); - } - - const comments = await caseService.getAllCaseComments({ - client, - caseId, - options: { - fields: [], - page: 1, - perPage: totalCommentsFindByCases.total, - }, - }); - - const externalService = { - pushed_at: pushedDate, - pushed_by: { username, full_name, email }, - ...query, - }; + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const updateConnector = myCase.attributes.connector; + const caseClient = context.case.getCaseClient(); + const actionsClient = context.actions?.getActionsClient(); - if ( - isEmpty(updateConnector) || - (updateConnector != null && updateConnector.id === 'none') || - !connectors.some((connector) => connector.id === updateConnector.id) - ) { - throw Boom.notFound('Connector not found or set to none'); - } + if (actionsClient == null) { + return response.badRequest({ body: 'Action client not found' }); + } - const [updatedCase, updatedComments] = await Promise.all([ - caseService.patchCase({ - client, - caseId, - updatedAttributes: { - ...(myCaseConfigure.total > 0 && - myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing' - ? { - status: CaseStatuses.closed, - closed_at: pushedDate, - closed_by: { email, full_name, username }, - } - : {}), - external_service: externalService, - updated_at: pushedDate, - updated_by: { username, full_name, email }, - }, - version: myCase.version, - }), - caseService.patchComments({ - client, - comments: comments.saved_objects - .filter((comment) => comment.attributes.pushed_at == null) - .map((comment) => ({ - commentId: comment.id, - updatedAttributes: { - pushed_at: pushedDate, - pushed_by: { username, full_name, email }, - }, - version: comment.version, - })), - }), - userActionService.postUserActions({ - client, - actions: [ - ...(myCaseConfigure.total > 0 && - myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing' - ? [ - buildCaseUserActionItem({ - action: 'update', - actionAt: pushedDate, - actionBy: { username, full_name, email }, - caseId, - fields: ['status'], - newValue: CaseStatuses.closed, - oldValue: myCase.attributes.status, - }), - ] - : []), - buildCaseUserActionItem({ - action: 'push-to-service', - actionAt: pushedDate, - actionBy: { username, full_name, email }, - caseId, - fields: ['pushed'], - newValue: JSON.stringify(externalService), - }), - ], - }), - ]); + try { + const params = pipe( + CasePushRequestParamsRt.decode(request.params), + fold(throwErrors(Boom.badRequest), identity) + ); return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - }, - comments: comments.saved_objects.map((origComment) => { - const updatedComment = updatedComments.saved_objects.find( - (c) => c.id === origComment.id - ); - return { - ...origComment, - ...updatedComment, - attributes: { - ...origComment.attributes, - ...updatedComment?.attributes, - ...getCommentContextFromAttributes(origComment.attributes), - }, - version: updatedComment?.version ?? origComment.version, - references: origComment?.references ?? [], - }; - }), - }) - ), + body: await caseClient.push({ + caseClient, + actionsClient, + caseId: params.case_id, + connectorId: params.connector_id, + }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts index e8761ad69dcca..9644162629f24 100644 --- a/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts @@ -36,24 +36,24 @@ describe('GET status', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, { + const response = await routeHandler(context, request, kibanaResponseFactory); + expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, { ...findArgs, filter: 'cases.attributes.status: open', }); - expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, { + expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, { ...findArgs, filter: 'cases.attributes.status: in-progress', }); - expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, { + expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, { ...findArgs, filter: 'cases.attributes.status: closed', }); @@ -71,13 +71,13 @@ describe('GET status', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [{ ...mockCases[0], id: 'throw-error-find' }], }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts index 346eec3dde752..06e929cc40e6b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts @@ -7,13 +7,11 @@ import { schema } from '@kbn/config-schema'; -import { CaseUserActionsResponseRt } from '../../../../../common/api'; -import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../../../saved_object_types'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; -export function initGetAllUserActionsApi({ userActionService, router }: RouteDeps) { +export function initGetAllUserActionsApi({ router }: RouteDeps) { router.get( { path: CASE_USER_ACTIONS_URL, @@ -24,22 +22,16 @@ export function initGetAllUserActionsApi({ userActionService, router }: RouteDep }, }, async (context, request, response) => { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; + try { - const client = context.core.savedObjects.client; - const userActions = await userActionService.getUserActions({ - client, - caseId: request.params.case_id, - }); return response.ok({ - body: CaseUserActionsResponseRt.encode( - userActions.saved_objects.map((ua) => ({ - ...ua.attributes, - action_id: ua.id, - case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '', - comment_id: - ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null, - })) - ), + body: await caseClient.getUserActions({ caseId }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts index c399364ea35ec..00660e08bbd83 100644 --- a/x-pack/plugins/case/server/routes/api/index.ts +++ b/x-pack/plugins/case/server/routes/api/index.ts @@ -10,7 +10,7 @@ import { initFindCasesApi } from '././cases/find_cases'; import { initGetCaseApi } from './cases/get_case'; import { initPatchCasesApi } from './cases/patch_cases'; import { initPostCaseApi } from './cases/post_case'; -import { initPushCaseUserActionApi } from './cases/push_case'; +import { initPushCaseApi } from './cases/push_case'; import { initGetReportersApi } from './cases/reporters/get_reporters'; import { initGetCasesStatusApi } from './cases/status/get_status'; import { initGetTagsApi } from './cases/tags/get_tags'; @@ -28,7 +28,6 @@ import { initCaseConfigureGetActionConnector } from './cases/configure/get_conne import { initGetCaseConfigure } from './cases/configure/get_configure'; import { initPatchCaseConfigure } from './cases/configure/patch_configure'; import { initPostCaseConfigure } from './cases/configure/post_configure'; -import { initPostPushToService } from './cases/configure/post_push_to_service'; import { RouteDeps } from './types'; @@ -39,7 +38,7 @@ export function initCaseApi(deps: RouteDeps) { initGetCaseApi(deps); initPatchCasesApi(deps); initPostCaseApi(deps); - initPushCaseUserActionApi(deps); + initPushCaseApi(deps); initGetAllUserActionsApi(deps); // Comments initDeleteCommentApi(deps); @@ -54,7 +53,6 @@ export function initCaseApi(deps: RouteDeps) { initGetCaseConfigure(deps); initPatchCaseConfigure(deps); initPostCaseConfigure(deps); - initPostPushToService(deps); // Reporters initGetReportersApi(deps); // Status diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index b7e556daffbd9..e2751c05d880a 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -191,11 +191,11 @@ export const sortToSnake = (sortField: string): SortFieldCase => { export const escapeHatch = schema.object({}, { unknowns: 'allow' }); -const isUserContext = (context: CommentRequest): context is CommentRequestUserType => { +export const isUserContext = (context: CommentRequest): context is CommentRequestUserType => { return context.type === CommentType.user; }; -const isAlertContext = (context: CommentRequest): context is CommentRequestAlertType => { +export const isAlertContext = (context: CommentRequest): context is CommentRequestAlertType => { return context.type === CommentType.alert; }; @@ -206,17 +206,3 @@ export const decodeComment = (comment: CommentRequest) => { pipe(excess(ContextTypeAlertRt).decode(comment), fold(throwErrors(badRequest), identity)); } }; - -export const getCommentContextFromAttributes = ( - attributes: CommentAttributes -): CommentRequestUserType | CommentRequestAlertType => - isUserContext(attributes) - ? { - type: CommentType.user, - comment: attributes.comment, - } - : { - type: CommentType.alert, - alertId: attributes.alertId, - index: attributes.index, - }; diff --git a/x-pack/plugins/case/server/services/alerts/index.ts b/x-pack/plugins/case/server/services/alerts/index.ts index 4f0d415f23b50..2776d6b40761e 100644 --- a/x-pack/plugins/case/server/services/alerts/index.ts +++ b/x-pack/plugins/case/server/services/alerts/index.ts @@ -19,6 +19,24 @@ interface UpdateAlertsStatusArgs { index: string; } +interface GetAlertsArgs { + request: KibanaRequest; + ids: string[]; + index: string; +} + +interface Alert { + _id: string; + _index: string; + _source: Record; +} + +interface AlertsResponse { + hits: { + hits: Alert[]; + }; +} + export class AlertService { private isInitialized = false; private esClient?: IClusterClient; @@ -55,4 +73,30 @@ export class AlertService { return result; } + + public async getAlerts({ request, ids, index }: GetAlertsArgs): Promise { + if (!this.isInitialized) { + throw new Error('AlertService not initialized'); + } + + // The above check makes sure that esClient is defined. + const result = await this.esClient!.asScoped(request).asCurrentUser.search({ + index, + body: { + query: { + bool: { + filter: { + bool: { + should: ids.map((_id) => ({ match: { _id } })), + minimum_should_match: 1, + }, + }, + }, + }, + }, + ignore_unavailable: true, + }); + + return result.body; + } } diff --git a/x-pack/plugins/case/server/services/mocks.ts b/x-pack/plugins/case/server/services/mocks.ts index 7c8b44b297362..0b3615793ef85 100644 --- a/x-pack/plugins/case/server/services/mocks.ts +++ b/x-pack/plugins/case/server/services/mocks.ts @@ -59,4 +59,5 @@ export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({ export const createAlertServiceMock = (): AlertServiceMock => ({ initialize: jest.fn(), updateAlertsStatus: jest.fn(), + getAlerts: jest.fn(), }); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts index 1380cfd9fca98..95b555c2acae6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts @@ -31,6 +31,13 @@ describe('Cases connector incident fields', () => { beforeEach(() => { cleanKibana(); cy.intercept('GET', '/api/cases/configure/connectors/_find', mockConnectorsResponse); + cy.intercept('POST', `/api/actions/action/${connectorIds.sn}/_execute`, (req) => { + const response = + req.body.params.subAction === 'getChoices' + ? executeResponses.servicenow.choices + : { status: 'ok', data: [] }; + req.reply(response); + }); cy.intercept('POST', `/api/actions/action/${connectorIds.jira}/_execute`, (req) => { const response = req.body.params.subAction === 'issueTypes' diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index b6c73cd37140c..7a3ce2cb00dfa 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -113,6 +113,77 @@ export const mockConnectorsResponse = [ }, ]; export const executeResponses = { + servicenow: { + choices: { + status: 'ok', + data: [ + { + dependent_value: '', + label: 'Priviledge Escalation', + value: 'Priviledge Escalation', + element: 'category', + }, + { + dependent_value: '', + label: 'Criminal activity/investigation', + value: 'Criminal activity/investigation', + element: 'category', + }, + { + dependent_value: '', + label: 'Denial of Service', + value: 'Denial of Service', + element: 'category', + }, + { + dependent_value: 'Denial of Service', + label: 'Inbound or outbound', + value: '12', + element: 'subcategory', + }, + { + dependent_value: 'Denial of Service', + label: 'Single or distributed (DoS or DDoS)', + value: '26', + element: 'subcategory', + }, + { + dependent_value: 'Denial of Service', + label: 'Inbound DDos', + value: 'inbound_ddos', + element: 'subcategory', + }, + ...['severity', 'urgency', 'impact', 'priority'] + .map((element) => [ + { + dependent_value: '', + label: '1 - Critical', + value: '1', + element, + }, + { + dependent_value: '', + label: '2 - High', + value: '2', + element, + }, + { + dependent_value: '', + label: '3 - Moderate', + value: '3', + element, + }, + { + dependent_value: '', + label: '4 - Low', + value: '4', + element, + }, + ]) + .flat(), + ], + }, + }, jira: { issueTypes: { status: 'ok', diff --git a/x-pack/plugins/security_solution/cypress/screens/case_details.ts b/x-pack/plugins/security_solution/cypress/screens/case_details.ts index 9ca7a99f9df16..ef8f45b222dd0 100644 --- a/x-pack/plugins/security_solution/cypress/screens/case_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/case_details.ts @@ -30,9 +30,9 @@ export const CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME = export const CASE_DETAILS_USERNAMES = '[data-test-subj="case-view-username"]'; -export const CONNECTOR_CARD_DETAILS = '[data-test-subj="settings-connector-card"]'; +export const CONNECTOR_CARD_DETAILS = '[data-test-subj="connector-card"]'; -export const CONNECTOR_TITLE = '[data-test-subj="settings-connector-card"] span.euiTitle'; +export const CONNECTOR_TITLE = '[data-test-subj="connector-card"] span.euiTitle'; export const DELETE_CASE_BTN = '[data-test-subj="property-actions-trash"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts b/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts index b25b8c11ff830..5b353983e5a92 100644 --- a/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts +++ b/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts @@ -7,7 +7,7 @@ import { connectorIds } from '../objects/case'; -export const CONNECTOR_RESILIENT = `[data-test-subj="connector-settings-resilient"]`; +export const CONNECTOR_RESILIENT = `[data-test-subj="connector-fields-resilient"]`; export const CONNECTOR_SELECTOR = '[data-test-subj="dropdown-connectors"]'; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index 511bc682e5504..e74b66eeeb9f0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -107,7 +107,7 @@ describe('CaseView ', () => { const fetchCaseUserActions = jest.fn(); const fetchCase = jest.fn(); const updateCase = jest.fn(); - const postPushToService = jest.fn(); + const pushCaseToExternalService = jest.fn(); const data = caseProps.caseData; const defaultGetCase = { @@ -144,7 +144,10 @@ describe('CaseView ', () => { jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); useGetCaseUserActionsMock.mockImplementation(() => defaultUseGetCaseUserActions); - usePostPushToServiceMock.mockImplementation(() => ({ isLoading: false, postPushToService })); + usePostPushToServiceMock.mockImplementation(() => ({ + isLoading: false, + pushCaseToExternalService, + })); useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false })); useQueryAlertsMock.mockImplementation(() => ({ loading: false, @@ -378,7 +381,7 @@ describe('CaseView ', () => { wrapper.update(); - expect(postPushToService).toHaveBeenCalled(); + expect(pushCaseToExternalService).toHaveBeenCalled(); }); }); @@ -508,7 +511,7 @@ describe('CaseView ', () => { connector: { id: 'servicenow-1', name: 'SN 1', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, }} @@ -556,7 +559,7 @@ describe('CaseView ', () => { connector: { id: 'servicenow-1', name: 'SN 1', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, }} diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 2f39a5a2951b2..e690a01dca54b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -297,7 +297,6 @@ export const CaseComponent = React.memo( updateCase: handleUpdateCase, userCanCrud, isValidConnector: isLoadingConnectors ? true : isValidConnector, - alerts, }); const onSubmitConnector = useCallback( @@ -397,7 +396,6 @@ export const CaseComponent = React.memo( ); } }, [dispatch]); - return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx index ef0c7cfcfa2d6..371ff3528f4f0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx @@ -72,7 +72,7 @@ describe('Connectors', () => { const newWrapper = mount( , { wrappingComponent: TestProviders, @@ -99,7 +99,7 @@ describe('Connectors', () => { const newWrapper = mount( , { wrappingComponent: TestProviders, diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx index 23cefce1bacd2..8e317d57dd9ac 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx @@ -186,14 +186,14 @@ describe('ConfigureCases', () => { connector: { id: 'servicenow-1', name: 'unchanged', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, currentConfiguration: { connector: { id: 'servicenow-1', name: 'unchanged', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, closureType: 'close-by-user', @@ -271,7 +271,7 @@ describe('ConfigureCases', () => { connector: { id: 'servicenow-1', name: 'unchanged', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, closureType: 'close-by-user', @@ -331,7 +331,7 @@ describe('ConfigureCases', () => { connector: { id: 'servicenow-1', name: 'SN', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, persistLoading: true, @@ -450,7 +450,7 @@ describe('ConfigureCases', () => { connector: { id: 'servicenow-1', name: 'My connector', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, })) @@ -493,7 +493,7 @@ describe('closure options', () => { connector: { id: 'servicenow-1', name: 'My connector', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, currentConfiguration: { @@ -522,7 +522,7 @@ describe('closure options', () => { connector: { id: 'servicenow-1', name: 'My connector', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, closureType: 'close-by-pushing', @@ -546,7 +546,7 @@ describe('user interactions', () => { connector: { id: 'resilient-2', name: 'unchanged', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, closureType: 'close-by-user', diff --git a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx index 0aaac9c30feb9..d5f5530acde9b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { isEmpty } from 'lodash/fp'; import { EuiFormRow } from '@elastic/eui'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; -import { ActionConnector } from '../../../../../case/common/api/cases'; +import { ActionConnector } from '../../../../../case/common/api'; interface ConnectorSelectorProps { connectors: ActionConnector[]; @@ -21,6 +21,7 @@ interface ConnectorSelectorProps { idAria: string; isEdit: boolean; isLoading: boolean; + handleChange?: (newValue: string) => void; } export const ConnectorSelector = ({ connectors, @@ -30,8 +31,19 @@ export const ConnectorSelector = ({ idAria, isEdit = true, isLoading = false, + handleChange, }: ConnectorSelectorProps) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const onChange = useCallback( + (val: string) => { + if (handleChange) { + handleChange(val); + } + field.setValue(val); + }, + [handleChange, field] + ); + return isEdit ? ( diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/card.tsx similarity index 89% rename from x-pack/plugins/security_solution/public/cases/components/settings/card.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/card.tsx index 36679cd2452bd..03f909948370d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/card.tsx @@ -9,7 +9,7 @@ import React, { memo, useMemo } from 'react'; import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; -import { connectorsConfiguration } from '../connectors'; +import { connectorsConfiguration } from '.'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; interface ConnectorCardProps { @@ -51,10 +51,10 @@ const ConnectorCardDisplay: React.FC = ({ ); return ( <> - {isLoading && } + {isLoading && } {!isLoading && ( ({ config: { errors: {} }, secrets: { errors: {} } }), validateParams, actionConnectorFields: null, - actionParamsFields: lazy(() => import('./fields')), + actionParamsFields: lazy(() => import('./alert_fields')), }; } diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/config.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/config.ts index 7be49720fc075..1d12d4b98a823 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/config.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/config.ts @@ -5,17 +5,35 @@ * 2.0. */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - import { - ServiceNowITSMConnectorConfiguration, - JiraConnectorConfiguration, - ResilientConnectorConfiguration, + getResilientActionType, + getServiceNowITSMActionType, + getServiceNowSIRActionType, + getJiraActionType, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../triggers_actions_ui/public/common'; import { ConnectorConfiguration } from './types'; +const resilient = getResilientActionType(); +const serviceNowITSM = getServiceNowITSMActionType(); +const serviceNowSIR = getServiceNowSIRActionType(); +const jira = getJiraActionType(); + export const connectorsConfiguration: Record = { - '.servicenow': ServiceNowITSMConnectorConfiguration as ConnectorConfiguration, - '.jira': JiraConnectorConfiguration as ConnectorConfiguration, - '.resilient': ResilientConnectorConfiguration as ConnectorConfiguration, + '.servicenow': { + name: serviceNowITSM.actionTypeTitle ?? '', + logo: serviceNowITSM.iconClass, + }, + '.servicenow-sir': { + name: serviceNowSIR.actionTypeTitle ?? '', + logo: serviceNowSIR.iconClass, + }, + '.jira': { + name: jira.actionTypeTitle ?? '', + logo: jira.iconClass, + }, + '.resilient': { + name: resilient.actionTypeTitle ?? '', + logo: resilient.iconClass, + }, }; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/connectors_registry.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/connectors_registry.ts new file mode 100644 index 0000000000000..d6896a8ac8c80 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/connectors_registry.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { CaseConnector, CaseConnectorsRegistry } from './types'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => { + const connectors: Map> = new Map(); + + const registry: CaseConnectorsRegistry = { + has: (id: string) => connectors.has(id), + register: (connector: CaseConnector) => { + if (connectors.has(connector.id)) { + throw new Error( + i18n.translate( + 'xpack.securitySolution.caseConnectorsRegistry.register.duplicateCaseConnectorErrorMessage', + { + defaultMessage: 'Object type "{id}" is already registered.', + values: { + id: connector.id, + }, + } + ) + ); + } + + connectors.set(connector.id, connector); + }, + get: (id: string): CaseConnector => { + if (!connectors.has(id)) { + throw new Error( + i18n.translate( + 'xpack.securitySolution.caseConnectorsRegistry.get.missingCaseConnectorErrorMessage', + { + defaultMessage: 'Object type "{id}" is not registered.', + values: { + id, + }, + } + ) + ); + } + return connectors.get(id)!; + }, + list: () => { + return Array.from(connectors).map(([id, connector]) => connector); + }, + }; + + return registry; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/fields_form.tsx similarity index 64% rename from x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/fields_form.tsx index 6b1a0cac8d9cd..41ed99e0f6768 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/fields_form.tsx @@ -8,24 +8,22 @@ import React, { memo, Suspense } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import { CaseSettingsConnector, SettingFieldsProps } from './types'; -import { getCaseSettings } from '.'; +import { CaseActionConnector, ConnectorFieldsProps } from './types'; +import { getCaseConnectors } from '.'; import { ConnectorTypeFields } from '../../../../../case/common/api/connectors'; -interface Props extends Omit, 'connector'> { - connector: CaseSettingsConnector | null; +interface Props extends Omit, 'connector'> { + connector: CaseActionConnector | null; } -const SettingFieldsFormComponent: React.FC = ({ connector, isEdit, onChange, fields }) => { - const { caseSettingsRegistry } = getCaseSettings(); +const ConnectorFieldsFormComponent: React.FC = ({ connector, isEdit, onChange, fields }) => { + const { caseConnectorsRegistry } = getCaseConnectors(); if (connector == null || connector.actionTypeId == null || connector.actionTypeId === '.none') { return null; } - const { caseSettingFieldsComponent: FieldsComponent } = caseSettingsRegistry.get( - connector.actionTypeId - ); + const { fieldsComponent: FieldsComponent } = caseConnectorsRegistry.get(connector.actionTypeId); return ( <> @@ -39,7 +37,7 @@ const SettingFieldsFormComponent: React.FC = ({ connector, isEdit, onChan
} > -
+
= ({ connector, isEdit, onChan ); }; -export const SettingFieldsForm = memo(SettingFieldsFormComponent); +export const ConnectorFieldsForm = memo(ConnectorFieldsFormComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts index 96cb215557c24..267126fc6ec8b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts @@ -5,7 +5,53 @@ * 2.0. */ +import { CaseConnectorsRegistry } from './types'; +import { createCaseConnectorsRegistry } from './connectors_registry'; +import { getCaseConnector as getJiraCaseConnector } from './jira'; +import { getCaseConnector as getResilientCaseConnector } from './resilient'; +import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; +import { + JiraFieldsType, + ServiceNowITSMFieldsType, + ServiceNowSIRFieldsType, + ResilientFieldsType, +} from '../../../../../case/common/api/connectors'; + export { getActionType as getCaseConnectorUI } from './case'; export * from './config'; export * from './types'; + +interface GetCaseConnectorsReturn { + caseConnectorsRegistry: CaseConnectorsRegistry; +} + +class CaseConnectors { + private caseConnectorsRegistry: CaseConnectorsRegistry; + + constructor() { + this.caseConnectorsRegistry = createCaseConnectorsRegistry(); + this.init(); + } + + private init() { + this.caseConnectorsRegistry.register(getJiraCaseConnector()); + this.caseConnectorsRegistry.register(getResilientCaseConnector()); + this.caseConnectorsRegistry.register( + getServiceNowITSMCaseConnector() + ); + this.caseConnectorsRegistry.register(getServiceNowSIRCaseConnector()); + } + + registry(): CaseConnectorsRegistry { + return this.caseConnectorsRegistry; + } +} + +const caseConnectors = new CaseConnectors(); + +export const getCaseConnectors = (): GetCaseConnectorsReturn => { + return { + caseConnectorsRegistry: caseConnectors.registry(), + }; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/__mocks__/api.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/__mocks__/api.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/api.test.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/api.test.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/api.test.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/api.test.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/api.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/api.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.test.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.test.tsx index 0c590d0ecd7ad..b151d41c4cdd8 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.test.tsx @@ -12,7 +12,7 @@ import { omit } from 'lodash/fp'; import { connector, issues } from '../mock'; import { useGetIssueTypes } from './use_get_issue_types'; import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; -import Fields from './fields'; +import Fields from './case_fields'; import { waitFor } from '@testing-library/dom'; import { useGetSingleIssue } from './use_get_single_issue'; import { useGetIssues } from './use_get_issues'; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.tsx similarity index 91% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.tsx index 6409fe71a85fc..d768b552b78b4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.tsx @@ -5,25 +5,26 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useEffect, useRef } from 'react'; import { map } from 'lodash/fp'; import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import * as i18n from './translations'; import { ConnectorTypes, JiraFieldsType } from '../../../../../../case/common/api/connectors'; import { useKibana } from '../../../../common/lib/kibana'; -import { SettingFieldsProps } from '../types'; +import { ConnectorFieldsProps } from '../types'; import { useGetIssueTypes } from './use_get_issue_types'; import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; import { SearchIssues } from './search_issues'; import { ConnectorCard } from '../card'; -const JiraSettingFieldsComponent: React.FunctionComponent> = ({ +const JiraFieldsComponent: React.FunctionComponent> = ({ connector, fields, isEdit = true, onChange, }) => { + const init = useRef(true); const { issueType = null, priority = null, parent = null } = fields ?? {}; const { http, notifications } = useKibana().services; @@ -138,8 +139,16 @@ const JiraSettingFieldsComponent: React.FunctionComponent { + if (init.current) { + init.current = false; + onChange({ issueType, priority, parent }); + } + }, [issueType, onChange, parent, priority]); + return isEdit ? ( -
+
=> { +export const getCaseConnector = (): CaseConnector => { return { id: '.jira', - caseSettingFieldsComponent: lazy(() => import('./fields')), + fieldsComponent: lazy(() => import('./case_fields')), }; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/search_issues.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/search_issues.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/search_issues.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/search_issues.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/translations.ts similarity index 60% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/translations.ts index 65fe339aceb67..07f8f5b984cdd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/translations.ts @@ -8,69 +8,69 @@ import { i18n } from '@kbn/i18n'; export const ISSUE_TYPES_API_ERROR = i18n.translate( - 'xpack.securitySolution.components.settings.jira.unableToGetIssueTypesMessage', + 'xpack.securitySolution.components.connectors.jira.unableToGetIssueTypesMessage', { defaultMessage: 'Unable to get issue types', } ); export const FIELDS_API_ERROR = i18n.translate( - 'xpack.securitySolution.components.settings.jira.unableToGetFieldsMessage', + 'xpack.securitySolution.components.connectors.jira.unableToGetFieldsMessage', { - defaultMessage: 'Unable to get fields', + defaultMessage: 'Unable to get connectors', } ); export const ISSUES_API_ERROR = i18n.translate( - 'xpack.securitySolution.components.settings.jira.unableToGetIssuesMessage', + 'xpack.securitySolution.components.connectors.jira.unableToGetIssuesMessage', { defaultMessage: 'Unable to get issues', } ); export const GET_ISSUE_API_ERROR = (id: string) => - i18n.translate('xpack.securitySolution.components.settings.jira.unableToGetIssueMessage', { + i18n.translate('xpack.securitySolution.components.connectors.jira.unableToGetIssueMessage', { defaultMessage: 'Unable to get issue with id {id}', values: { id }, }); export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate( - 'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel', + 'xpack.securitySolution.components.connectors.jira.searchIssuesComboBoxAriaLabel', { defaultMessage: 'Type to search', } ); export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder', + 'xpack.securitySolution.components.connectors.jira.searchIssuesComboBoxPlaceholder', { defaultMessage: 'Type to search', } ); export const SEARCH_ISSUES_LOADING = i18n.translate( - 'xpack.securitySolution.components.settings.jira.searchIssuesLoading', + 'xpack.securitySolution.components.connectors.jira.searchIssuesLoading', { defaultMessage: 'Loading...', } ); export const PRIORITY = i18n.translate( - 'xpack.securitySolution.case.settings.jira.prioritySelectFieldLabel', + 'xpack.securitySolution.case.connectors.jira.prioritySelectFieldLabel', { defaultMessage: 'Priority', } ); export const ISSUE_TYPE = i18n.translate( - 'xpack.securitySolution.case.settings.jira.issueTypesSelectFieldLabel', + 'xpack.securitySolution.case.connectors.jira.issueTypesSelectFieldLabel', { defaultMessage: 'Issue type', } ); export const PARENT_ISSUE = i18n.translate( - 'xpack.securitySolution.case.settings.jira.parentIssueSearchLabel', + 'xpack.securitySolution.case.connectors.jira.parentIssueSearchLabel', { defaultMessage: 'Parent issue', } diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/types.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/types.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_fields_by_issue_type.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_fields_by_issue_type.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_fields_by_issue_type.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issue_types.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issue_types.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issue_types.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issue_types.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts new file mode 100644 index 0000000000000..04e7338025258 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const connector = { + id: '123', + name: 'My connector', + actionTypeId: '.jira', + config: {}, + isPreconfigured: false, +}; + +export const issues = [ + { id: 'personId', title: 'Person Task', key: 'personKey' }, + { id: 'womanId', title: 'Woman Task', key: 'womanKey' }, + { id: 'manId', title: 'Man Task', key: 'manKey' }, + { id: 'cameraId', title: 'Camera Task', key: 'cameraKey' }, + { id: 'tvId', title: 'TV Task', key: 'tvKey' }, +]; + +export const choices = [ + { + dependent_value: '', + label: 'Priviledge Escalation', + value: 'Priviledge Escalation', + element: 'category', + }, + { + dependent_value: '', + label: 'Criminal activity/investigation', + value: 'Criminal activity/investigation', + element: 'category', + }, + { + dependent_value: '', + label: 'Denial of Service', + value: 'Denial of Service', + element: 'category', + }, + { + dependent_value: 'Denial of Service', + label: 'Inbound or outbound', + value: '12', + element: 'subcategory', + }, + { + dependent_value: 'Denial of Service', + label: 'Single or distributed (DoS or DDoS)', + value: '26', + element: 'subcategory', + }, + { + dependent_value: 'Denial of Service', + label: 'Inbound DDos', + value: 'inbound_ddos', + element: 'subcategory', + }, + ...['severity', 'urgency', 'impact', 'priority'] + .map((element) => [ + { + dependent_value: '', + label: '1 - Critical', + value: '1', + element, + }, + { + dependent_value: '', + label: '2 - High', + value: '2', + element, + }, + { + dependent_value: '', + label: '3 - Moderate', + value: '3', + element, + }, + { + dependent_value: '', + label: '4 - Low', + value: '4', + element, + }, + ]) + .flat(), +]; + +export const severity = [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, +]; + +export const incidentTypes = [ + { id: 17, name: 'Communication error (fax; email)' }, + { id: 1001, name: 'Custom type' }, +]; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/__mocks__/api.ts similarity index 70% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/__mocks__/api.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/__mocks__/api.ts index f4397eaf1877c..c27248288907d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/__mocks__/api.ts @@ -5,29 +5,10 @@ * 2.0. */ +import { incidentTypes, severity } from '../../mock'; import { Props } from '../api'; import { ResilientIncidentTypes, ResilientSeverity } from '../types'; -const severity = [ - { - id: 4, - name: 'Low', - }, - { - id: 5, - name: 'Medium', - }, - { - id: 6, - name: 'High', - }, -]; - -const incidentTypes = [ - { id: 17, name: 'Communication error (fax; email)' }, - { id: 1001, name: 'Custom type' }, -]; - export const getIncidentTypes = async (props: Props): Promise<{ data: ResilientIncidentTypes }> => Promise.resolve({ data: incidentTypes }); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/api.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/api.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.test.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.test.tsx index 9095f3b56f2c3..dd13083288020 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.test.tsx @@ -13,7 +13,7 @@ import { waitFor } from '@testing-library/react'; import { connector } from '../mock'; import { useGetIncidentTypes } from './use_get_incident_types'; import { useGetSeverity } from './use_get_severity'; -import Fields from './fields'; +import Fields from './case_fields'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_get_incident_types'); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.tsx index f79ce8a4a5630..8c62f5285c257 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo, useCallback, useEffect } from 'react'; +import React, { useMemo, useCallback, useEffect, useRef } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, @@ -16,8 +16,7 @@ import { } from '@elastic/eui'; import { useKibana } from '../../../../common/lib/kibana'; -import { SettingFieldsProps } from '../types'; - +import { ConnectorFieldsProps } from '../types'; import { useGetIncidentTypes } from './use_get_incident_types'; import { useGetSeverity } from './use_get_severity'; @@ -25,9 +24,10 @@ import * as i18n from './translations'; import { ConnectorTypes, ResilientFieldsType } from '../../../../../../case/common/api/connectors'; import { ConnectorCard } from '../card'; -const ResilientSettingFieldsComponent: React.FunctionComponent< - SettingFieldsProps +const ResilientFieldsComponent: React.FunctionComponent< + ConnectorFieldsProps > = ({ isEdit = true, fields, connector, onChange }) => { + const init = useRef(true); const { incidentTypes = null, severityCode = null } = fields ?? {}; const { http, notifications } = useKibana().services; @@ -136,14 +136,16 @@ const ResilientSettingFieldsComponent: React.FunctionComponent< } }, [incidentTypes, onFieldChange]); - // We need to set them up at initialization + // Set field at initialization useEffect(() => { - onChange({ incidentTypes, severityCode }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (init.current) { + init.current = false; + onChange({ incidentTypes, severityCode }); + } + }, [incidentTypes, onChange, severityCode]); return isEdit ? ( - + => { +export const getCaseConnector = (): CaseConnector => { return { id: '.resilient', - caseSettingFieldsComponent: lazy(() => import('./fields')), + fieldsComponent: lazy(() => import('./case_fields')), }; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/translations.ts similarity index 67% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/translations.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/translations.ts index 648baf840884b..32a72c3803708 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/translations.ts @@ -8,35 +8,35 @@ import { i18n } from '@kbn/i18n'; export const INCIDENT_TYPES_API_ERROR = i18n.translate( - 'xpack.securitySolution.case.settings.resilient.unableToGetIncidentTypesMessage', + 'xpack.securitySolution.case.connectors.resilient.unableToGetIncidentTypesMessage', { defaultMessage: 'Unable to get incident types', } ); export const SEVERITY_API_ERROR = i18n.translate( - 'xpack.securitySolution.case.settings.resilient.unableToGetSeverityMessage', + 'xpack.securitySolution.case.connectors.resilient.unableToGetSeverityMessage', { defaultMessage: 'Unable to get severity', } ); export const INCIDENT_TYPES_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.case.settings.resilient.incidentTypesPlaceholder', + 'xpack.securitySolution.case.connectors.resilient.incidentTypesPlaceholder', { defaultMessage: 'Choose types', } ); export const INCIDENT_TYPES_LABEL = i18n.translate( - 'xpack.securitySolution.case.settings.resilient.incidentTypesLabel', + 'xpack.securitySolution.case.connectors.resilient.incidentTypesLabel', { defaultMessage: 'Incident Types', } ); export const SEVERITY_LABEL = i18n.translate( - 'xpack.securitySolution.case.settings.resilient.severityLabel', + 'xpack.securitySolution.case.connectors.resilient.severityLabel', { defaultMessage: 'Severity', } diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/types.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/types.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_incident_types.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_incident_types.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_incident_types.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_incident_types.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_severity.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_severity.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_severity.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_severity.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/__mocks__/api.ts new file mode 100644 index 0000000000000..215e3d6f92e6d --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/__mocks__/api.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { choices } from '../../mock'; +import { GetChoicesProps } from '../api'; +import { Choice } from '../types'; + +export const choicesResponse = { + status: 'ok', + data: choices, +}; + +export const getChoices = async ( + props: GetChoicesProps +): Promise<{ status: string; data: Choice[] }> => Promise.resolve(choicesResponse); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.test.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.test.ts new file mode 100644 index 0000000000000..6a6bb7e947997 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; +import { getChoices } from './api'; +import { choices } from '../mock'; + +const choicesResponse = { + status: 'ok', + data: choices, +}; + +describe('ServiceNow API', () => { + const http = httpServiceMock.createStartContract(); + + beforeEach(() => jest.resetAllMocks()); + + describe('getChoices', () => { + test('should call get choices API', async () => { + const abortCtrl = new AbortController(); + http.post.mockResolvedValueOnce(choicesResponse); + const res = await getChoices({ + http, + signal: abortCtrl.signal, + connectorId: 'test', + fields: ['priority'], + }); + + expect(res).toEqual(choicesResponse); + expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + body: '{"params":{"subAction":"getChoices","subActionParams":{"fields":["priority"]}}}', + signal: abortCtrl.signal, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.ts new file mode 100644 index 0000000000000..d91ad9f8762bd --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpSetup } from 'kibana/public'; +import { ActionTypeExecutorResult } from '../../../../../../actions/common'; +import { Choice } from './types'; + +export const BASE_ACTION_API_PATH = '/api/actions'; + +export interface GetChoicesProps { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; + fields: string[]; +} + +export async function getChoices({ http, signal, connectorId, fields }: GetChoicesProps) { + return http.post>( + `${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, + { + body: JSON.stringify({ + params: { subAction: 'getChoices', subActionParams: { fields } }, + }), + signal, + } + ); +} diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts new file mode 100644 index 0000000000000..81bd81124599f --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +import { CaseConnector } from '../types'; +import { + ServiceNowITSMFieldsType, + ServiceNowSIRFieldsType, +} from '../../../../../../case/common/api/connectors'; +import * as i18n from './translations'; + +export const getServiceNowITSMCaseConnector = (): CaseConnector => { + return { + id: '.servicenow', + fieldsComponent: lazy(() => import('./servicenow_itsm_case_fields')), + }; +}; + +export const getServiceNowSIRCaseConnector = (): CaseConnector => { + return { + id: '.servicenow-sir', + fieldsComponent: lazy(() => import('./servicenow_sir_case_fields')), + }; +}; + +export const serviceNowITSMFieldLabels = { + impact: i18n.IMPACT, + severity: i18n.SEVERITY, + urgency: i18n.URGENCY, +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx similarity index 52% rename from x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx index 2e56e21aa8e98..555ed0dcbb161 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx @@ -6,36 +6,74 @@ */ import React from 'react'; -import { mount } from 'enzyme'; -import Fields from './fields'; -import { connector } from '../mock'; -import { waitFor } from '@testing-library/dom'; +import { waitFor, act } from '@testing-library/react'; import { EuiSelect } from '@elastic/eui'; +import { mount } from 'enzyme'; + +import { connector, choices as mockChoices } from '../mock'; +import { Choice } from './types'; +import Fields from './servicenow_itsm_case_fields'; + +let onChoicesSuccess = (c: Choice[]) => {}; -describe('ServiceNow Fields', () => { +jest.mock('../../../../common/lib/kibana'); +jest.mock('./use_get_choices', () => ({ + useGetChoices: (args: { onSuccess: () => void }) => { + onChoicesSuccess = args.onSuccess; + return { isLoading: false, mockChoices }; + }, +})); + +describe('ServiceNowITSM Fields', () => { const fields = { severity: '1', urgency: '2', impact: '3' }; const onChange = jest.fn(); + beforeEach(() => { jest.clearAllMocks(); }); + it('all params fields are rendered - isEdit: true', () => { const wrapper = mount(); - expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toEqual('1'); - expect(wrapper.find('[data-test-subj="urgencySelect"]').first().prop('value')).toEqual('2'); - expect(wrapper.find('[data-test-subj="impactSelect"]').first().prop('value')).toEqual('3'); + expect(wrapper.find('[data-test-subj="severitySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="urgencySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="impactSelect"]').exists()).toBeTruthy(); }); - test('all params fields are rendered - isEdit: false', () => { + it('all params fields are rendered - isEdit: false', () => { const wrapper = mount( ); + act(() => { + onChoicesSuccess(mockChoices); + }); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( - 'Urgency: Medium' + 'Urgency: 2 - High' ); expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( - 'Severity: High' + 'Severity: 1 - Critical' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( + 'Impact: 3 - Moderate' + ); + }); + + it('it transforms the options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + const testers = ['severity', 'urgency', 'impact']; + testers.forEach((subj) => + expect(wrapper.find(`[data-test-subj="${subj}Select"]`).first().prop('options')).toEqual([ + { value: '1', text: '1 - Critical' }, + { value: '2', text: '2 - High' }, + { value: '3', text: '3 - Moderate' }, + { value: '4', text: '4 - Low' }, + ]) ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual('Impact: Low'); }); describe('onChange calls', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx new file mode 100644 index 0000000000000..e278492b57148 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import { EuiFormRow, EuiSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import * as i18n from './translations'; + +import { ConnectorFieldsProps } from '../types'; +import { + ConnectorTypes, + ServiceNowITSMFieldsType, +} from '../../../../../../case/common/api/connectors'; +import { useKibana } from '../../../../common/lib/kibana'; +import { ConnectorCard } from '../card'; +import { useGetChoices } from './use_get_choices'; +import { Options, Choice } from './types'; + +const useGetChoicesFields = ['urgency', 'severity', 'impact']; +const defaultOptions: Options = { + urgency: [], + severity: [], + impact: [], +}; + +const ServiceNowITSMFieldsComponent: React.FunctionComponent< + ConnectorFieldsProps +> = ({ isEdit = true, fields, connector, onChange }) => { + const init = useRef(true); + const { severity = null, urgency = null, impact = null } = fields ?? {}; + const { http, notifications } = useKibana().services; + const [options, setOptions] = useState(defaultOptions); + + const listItems = useMemo( + () => [ + ...(urgency != null && urgency.length > 0 + ? [ + { + title: i18n.URGENCY, + description: options.urgency.find((option) => `${option.value}` === urgency)?.text, + }, + ] + : []), + ...(severity != null && severity.length > 0 + ? [ + { + title: i18n.SEVERITY, + description: options.severity.find((option) => `${option.value}` === severity)?.text, + }, + ] + : []), + ...(impact != null && impact.length > 0 + ? [ + { + title: i18n.IMPACT, + description: options.impact.find((option) => `${option.value}` === impact)?.text, + }, + ] + : []), + ], + [urgency, options.urgency, options.severity, options.impact, severity, impact] + ); + + const onChoicesSuccess = (choices: Choice[]) => + setOptions( + choices.reduce( + (acc, choice) => ({ + ...acc, + [choice.element]: [ + ...(acc[choice.element] != null ? acc[choice.element] : []), + { value: choice.value, text: choice.label }, + ], + }), + defaultOptions + ) + ); + + const { isLoading: isLoadingChoices } = useGetChoices({ + http, + toastNotifications: notifications.toasts, + connector, + fields: useGetChoicesFields, + onSuccess: onChoicesSuccess, + }); + + const onChangeCb = useCallback( + ( + key: keyof ServiceNowITSMFieldsType, + value: ServiceNowITSMFieldsType[keyof ServiceNowITSMFieldsType] + ) => { + onChange({ ...fields, [key]: value }); + }, + [fields, onChange] + ); + + // Set field at initialization + useEffect(() => { + if (init.current) { + init.current = false; + onChange({ urgency, severity, impact }); + } + }, [impact, onChange, severity, urgency]); + + return isEdit ? ( +
+ + onChangeCb('urgency', e.target.value)} + /> + + + + + + onChangeCb('severity', e.target.value)} + /> + + + + + onChangeCb('impact', e.target.value)} + /> + + + +
+ ) : ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ServiceNowITSMFieldsComponent as default }; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx new file mode 100644 index 0000000000000..7d785406afec8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { waitFor, act } from '@testing-library/react'; +import { EuiSelect } from '@elastic/eui'; + +import { connector, choices as mockChoices } from '../mock'; +import { Choice } from './types'; +import Fields from './servicenow_sir_case_fields'; + +let onChoicesSuccess = (c: Choice[]) => {}; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('./use_get_choices', () => ({ + useGetChoices: (args: { onSuccess: () => void }) => { + onChoicesSuccess = args.onSuccess; + return { isLoading: false, mockChoices }; + }, +})); + +describe('ServiceNowSIR Fields', () => { + const fields = { + destIp: true, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + priority: '1', + category: 'Denial of Service', + subcategory: '26', + }; + const onChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('all params fields are rendered - isEdit: true', () => { + const wrapper = mount(); + expect(wrapper.find('[data-test-subj="destIpCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="sourceIpCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="malwareUrlCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="malwareHashCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy(); + }); + + test('all params fields are rendered - isEdit: false', () => { + const wrapper = mount( + + ); + act(() => { + onChoicesSuccess(mockChoices); + }); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( + 'Destination IP: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( + 'Source IP: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( + 'Malware URL: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(3).text()).toEqual( + 'Malware Hash: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(4).text()).toEqual( + 'Priority: 1 - Critical' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(5).text()).toEqual( + 'Category: Denial of Service' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(6).text()).toEqual( + 'Subcategory: Single or distributed (DoS or DDoS)' + ); + }); + + test('it transforms the categories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="categorySelect"]').first().prop('options')).toEqual([ + { value: 'Priviledge Escalation', text: 'Priviledge Escalation' }, + { + value: 'Criminal activity/investigation', + text: 'Criminal activity/investigation', + }, + { value: 'Denial of Service', text: 'Denial of Service' }, + ]); + }); + + test('it transforms the subcategories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').first().prop('options')).toEqual([ + { + text: 'Inbound or outbound', + value: '12', + }, + { + text: 'Single or distributed (DoS or DDoS)', + value: '26', + }, + { + text: 'Inbound DDos', + value: 'inbound_ddos', + }, + ]); + }); + + test('it transforms the priorities to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('options')).toEqual([ + { + text: '1 - Critical', + value: '1', + }, + { + text: '2 - High', + value: '2', + }, + { + text: '3 - Moderate', + value: '3', + }, + { + text: '4 - Low', + value: '4', + }, + ]); + }); + + describe('onChange calls', () => { + const wrapper = mount(); + + act(() => { + onChoicesSuccess(mockChoices); + }); + wrapper.update(); + + expect(onChange).toHaveBeenCalledWith(fields); + + const checkbox = ['destIp', 'sourceIp', 'malwareHash', 'malwareUrl']; + checkbox.forEach((subj) => + test(`${subj.toUpperCase()}`, async () => { + await waitFor(() => { + wrapper + .find(`[data-test-subj="${subj}Checkbox"] input`) + .first() + .simulate('change', { target: { checked: false } }); + expect(onChange).toHaveBeenCalledWith({ + ...fields, + [subj]: false, + }); + }); + }) + ); + + const testers = ['priority', 'category', 'subcategory']; + testers.forEach((subj) => + test(`${subj.toUpperCase()}`, async () => { + await waitFor(() => { + const select = wrapper.find(EuiSelect).filter(`[data-test-subj="${subj}Select"]`)!; + select.prop('onChange')!({ + target: { + value: '9', + }, + } as React.ChangeEvent); + }); + wrapper.update(); + expect(onChange).toHaveBeenCalledWith({ + ...fields, + [subj]: '9', + }); + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx new file mode 100644 index 0000000000000..96db43fe261ac --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx @@ -0,0 +1,293 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import { + EuiFormRow, + EuiSelect, + EuiFlexGroup, + EuiFlexItem, + EuiSelectOption, + EuiCheckbox, +} from '@elastic/eui'; + +import { + ConnectorTypes, + ServiceNowSIRFieldsType, +} from '../../../../../../case/common/api/connectors'; +import { useKibana } from '../../../../common/lib/kibana'; +import { ConnectorFieldsProps } from '../types'; +import { ConnectorCard } from '../card'; +import { useGetChoices } from './use_get_choices'; +import { Choice, Fields } from './types'; + +import * as i18n from './translations'; + +const useGetChoicesFields = ['category', 'subcategory', 'priority']; +const defaultFields: Fields = { + category: [], + subcategory: [], + priority: [], +}; + +const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] => + choices.map((choice) => ({ value: choice.value, text: choice.label })); + +const ServiceNowSIRFieldsComponent: React.FunctionComponent< + ConnectorFieldsProps +> = ({ isEdit = true, fields, connector, onChange }) => { + const init = useRef(true); + const { + category = null, + destIp = true, + malwareHash = true, + malwareUrl = true, + priority = null, + sourceIp = true, + subcategory = null, + } = fields ?? {}; + + const { http, notifications } = useKibana().services; + + const [choices, setChoices] = useState(defaultFields); + + const onChangeCb = useCallback( + ( + key: keyof ServiceNowSIRFieldsType, + value: ServiceNowSIRFieldsType[keyof ServiceNowSIRFieldsType] + ) => { + onChange({ ...fields, [key]: value }); + }, + [fields, onChange] + ); + + const onChoicesSuccess = (values: Choice[]) => { + setChoices( + values.reduce( + (acc, value) => ({ + ...acc, + [value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value], + }), + defaultFields + ) + ); + }; + + const { isLoading: isLoadingChoices } = useGetChoices({ + http, + toastNotifications: notifications.toasts, + connector, + fields: useGetChoicesFields, + onSuccess: onChoicesSuccess, + }); + + const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]); + const priorityOptions = useMemo(() => choicesToEuiOptions(choices.priority), [choices.priority]); + + const subcategoryOptions = useMemo( + () => + choicesToEuiOptions( + choices.subcategory.filter((choice) => choice.dependent_value === category) + ), + [choices.subcategory, category] + ); + + const listItems = useMemo( + () => [ + ...(destIp != null && destIp + ? [ + { + title: i18n.DEST_IP, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(sourceIp != null && sourceIp + ? [ + { + title: i18n.SOURCE_IP, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(malwareUrl != null && malwareUrl + ? [ + { + title: i18n.MALWARE_URL, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(malwareHash != null && malwareHash + ? [ + { + title: i18n.MALWARE_HASH, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(priority != null && priority.length > 0 + ? [ + { + title: i18n.PRIORITY, + description: priorityOptions.find((option) => `${option.value}` === priority)?.text, + }, + ] + : []), + ...(category != null && category.length > 0 + ? [ + { + title: i18n.CATEGORY, + description: categoryOptions.find((option) => `${option.value}` === category)?.text, + }, + ] + : []), + ...(subcategory != null && subcategory.length > 0 + ? [ + { + title: i18n.SUBCATEGORY, + description: subcategoryOptions.find((option) => `${option.value}` === subcategory) + ?.text, + }, + ] + : []), + ], + [ + category, + categoryOptions, + destIp, + malwareHash, + malwareUrl, + priority, + priorityOptions, + sourceIp, + subcategory, + subcategoryOptions, + ] + ); + + // Set field at initialization + useEffect(() => { + if (init.current) { + init.current = false; + onChange({ category, destIp, malwareHash, malwareUrl, priority, sourceIp, subcategory }); + } + }, [category, destIp, malwareHash, malwareUrl, onChange, priority, sourceIp, subcategory]); + + return isEdit ? ( +
+ + + + <> + + + onChangeCb('destIp', e.target.checked)} + /> + + + onChangeCb('sourceIp', e.target.checked)} + /> + + + + + onChangeCb('malwareUrl', e.target.checked)} + /> + + + onChangeCb('malwareHash', e.target.checked)} + /> + + + + + + + + + + onChangeCb('priority', e.target.value)} + /> + + + + + + + onChangeCb('category', e.target.value)} + /> + + + + + onChangeCb('subcategory', e.target.value)} + /> + + + +
+ ) : ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ServiceNowSIRFieldsComponent as default }; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts new file mode 100644 index 0000000000000..0867dc41eeb78 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const URGENCY = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.urgencySelectFieldLabel', + { + defaultMessage: 'Urgency', + } +); + +export const SEVERITY = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.severitySelectFieldLabel', + { + defaultMessage: 'Severity', + } +); + +export const IMPACT = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.impactSelectFieldLabel', + { + defaultMessage: 'Impact', + } +); + +export const CHOICES_API_ERROR = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.unableToGetChoicesMessage', + { + defaultMessage: 'Unable to get choices', + } +); + +export const MALWARE_URL = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.malwareURLTitle', + { + defaultMessage: 'Malware URL', + } +); + +export const MALWARE_HASH = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.malwareHashTitle', + { + defaultMessage: 'Malware Hash', + } +); + +export const CATEGORY = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.categoryTitle', + { + defaultMessage: 'Category', + } +); + +export const SUBCATEGORY = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.subcategoryTitle', + { + defaultMessage: 'Subcategory', + } +); + +export const SOURCE_IP = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.sourceIPTitle', + { + defaultMessage: 'Source IP', + } +); + +export const DEST_IP = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.destinationIPTitle', + { + defaultMessage: 'Destination IP', + } +); + +export const PRIORITY = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.prioritySelectFieldTitle', + { + defaultMessage: 'Priority', + } +); + +export const ALERT_FIELDS_LABEL = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.alertFieldsTitle', + { + defaultMessage: 'Fields associated with alerts', + } +); + +export const ALERT_FIELD_ENABLED_TEXT = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.alertFieldEnabledText', + { + defaultMessage: 'Yes', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts new file mode 100644 index 0000000000000..deceeed29482b --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSelectOption } from '@elastic/eui'; + +export interface Choice { + value: string; + label: string; + dependent_value: string; + element: string; +} + +export type Fields = Record; +export type Options = Record; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.test.tsx new file mode 100644 index 0000000000000..2492fbaaf5a83 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.test.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { ActionConnector } from '../../../containers/types'; +import { choices } from '../mock'; +import { useGetChoices, UseGetChoices, UseGetChoicesProps } from './use_get_choices'; +import * as api from './api'; + +jest.mock('./api'); +jest.mock('../../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mocked; +const onSuccess = jest.fn(); +const fields = ['priority']; + +const connector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + name: 'ServiceNow', + isPreconfigured: false, + config: { + apiUrl: 'https://dev94428.service-now.com/', + }, +} as ActionConnector; + +describe('useGetChoices', () => { + const { services } = useKibanaMock(); + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('init', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + isLoading: false, + choices, + }); + }); + + it('returns an empty array when connector is not presented', async () => { + const { result } = renderHook(() => + useGetChoices({ + http: services.http, + connector: undefined, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + expect(result.current).toEqual({ + isLoading: false, + choices: [], + }); + }); + + it('it calls onSuccess', async () => { + const { waitForNextUpdate } = renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(onSuccess).toHaveBeenCalledWith(choices); + }); + + it('it displays an error when service fails', async () => { + const spyOnGetChoices = jest.spyOn(api, 'getChoices'); + spyOnGetChoices.mockResolvedValue( + Promise.resolve({ + actionId: 'test', + status: 'error', + serviceMessage: 'An error occurred', + }) + ); + + const { waitForNextUpdate } = renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'An error occurred', + title: 'Unable to get choices', + }); + }); + + it('it displays an error when http throws an error', async () => { + const spyOnGetChoices = jest.spyOn(api, 'getChoices'); + spyOnGetChoices.mockImplementation(() => { + throw new Error('An error occurred'); + }); + + renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'An error occurred', + title: 'Unable to get choices', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx new file mode 100644 index 0000000000000..16e905bdabfee --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; +import { getChoices } from './api'; +import { Choice } from './types'; +import * as i18n from './translations'; + +export interface UseGetChoicesProps { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + connector?: ActionConnector; + fields: string[]; + onSuccess?: (choices: Choice[]) => void; +} + +export interface UseGetChoices { + choices: Choice[]; + isLoading: boolean; +} + +export const useGetChoices = ({ + http, + connector, + toastNotifications, + fields, + onSuccess, +}: UseGetChoicesProps): UseGetChoices => { + const [isLoading, setIsLoading] = useState(false); + const [choices, setChoices] = useState([]); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + if (!connector) { + setIsLoading(false); + return; + } + + abortCtrl.current = new AbortController(); + setIsLoading(true); + + try { + const res = await getChoices({ + http, + signal: abortCtrl.current.signal, + connectorId: connector.id, + fields, + }); + + if (!didCancel) { + setIsLoading(false); + setChoices(res.data ?? []); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.CHOICES_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } else if (onSuccess) { + onSuccess(res.data ?? []); + } + } + } catch (error) { + if (!didCancel) { + setIsLoading(false); + toastNotifications.addDanger({ + title: i18n.CHOICES_API_ERROR, + text: error.message, + }); + } + } + }; + + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel = true; + setIsLoading(false); + abortCtrl.current.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [http, connector, toastNotifications, fields]); + + return { + choices, + isLoading, + }; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts index 808e185eabb6f..46c707197fdb4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts @@ -5,14 +5,17 @@ * 2.0. */ -import { ActionType } from '../../../../../triggers_actions_ui/public'; +import React from 'react'; import { ActionType as ThirdPartySupportedActions, CaseField, + ActionConnector, + ConnectorTypeFields, } from '../../../../../case/common/api'; export { ThirdPartyField as AllThirdPartyFields } from '../../../../../case/common/api'; +export type CaseActionConnector = ActionConnector; export interface ThirdPartyField { label: string; @@ -21,6 +24,30 @@ export interface ThirdPartyField { defaultActionType: ThirdPartySupportedActions; } -export interface ConnectorConfiguration extends ActionType { +export interface ConnectorConfiguration { + name: string; logo: string; } + +export interface CaseConnector { + id: string; + fieldsComponent: React.LazyExoticComponent< + React.ComponentType> + > | null; +} + +export interface CaseConnectorsRegistry { + has: (id: string) => boolean; + register: ( + connector: CaseConnector + ) => void; + get: (id: string) => CaseConnector; + list: () => CaseConnector[]; +} + +export interface ConnectorFieldsProps { + isEdit?: boolean; + connector: CaseActionConnector; + fields: TFields; + onChange: (fields: TFields) => void; +} diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx index 2a361a2f6cdce..236c13e5afc08 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx @@ -14,8 +14,10 @@ import { useForm, Form, FormHook } from '../../../shared_imports'; import { connectorsMock } from '../../containers/mock'; import { Connector } from './connector'; import { useConnectors } from '../../containers/configure/use_connectors'; -import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types'; -import { useGetSeverity } from '../settings/resilient/use_get_severity'; +import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; +import { useGetSeverity } from '../connectors/resilient/use_get_severity'; +import { useGetChoices } from '../connectors/servicenow/use_get_choices'; +import { incidentTypes, severity, choices } from '../connectors/mock'; import { schema, FormProps } from './schema'; jest.mock('../../../common/lib/kibana', () => { @@ -29,43 +31,28 @@ jest.mock('../../../common/lib/kibana', () => { }; }); jest.mock('../../containers/configure/use_connectors'); -jest.mock('../settings/resilient/use_get_incident_types'); -jest.mock('../settings/resilient/use_get_severity'); +jest.mock('../connectors/resilient/use_get_incident_types'); +jest.mock('../connectors/resilient/use_get_severity'); +jest.mock('../connectors/servicenow/use_get_choices'); const useConnectorsMock = useConnectors as jest.Mock; const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; const useGetSeverityMock = useGetSeverity as jest.Mock; +const useGetChoicesMock = useGetChoices as jest.Mock; const useGetIncidentTypesResponse = { isLoading: false, - incidentTypes: [ - { - id: 19, - name: 'Malware', - }, - { - id: 21, - name: 'Denial of Service', - }, - ], + incidentTypes, }; const useGetSeverityResponse = { isLoading: false, - severity: [ - { - id: 4, - name: 'Low', - }, - { - id: 5, - name: 'Medium', - }, - { - id: 6, - name: 'High', - }, - ], + severity, +}; + +const useGetChoicesResponse = { + isLoading: false, + choices, }; describe('Connector', () => { @@ -90,6 +77,7 @@ describe('Connector', () => { useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + useGetChoicesMock.mockReturnValue(useGetChoicesResponse); }); it('it renders', async () => { @@ -100,7 +88,7 @@ describe('Connector', () => { ); expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="connector-settings"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeTruthy(); await waitFor(() => { expect(wrapper.find(`button[data-test-subj="dropdown-connectors"]`).first().text()).toBe( @@ -108,10 +96,10 @@ describe('Connector', () => { ); }); - await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy(); - }); + // await waitFor(() => { + // wrapper.update(); + // expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeTruthy(); + // }); }); it('it is loading when fetching connectors', async () => { @@ -163,7 +151,7 @@ describe('Connector', () => { ); await waitFor(() => { - expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy(); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); wrapper.update(); @@ -171,7 +159,7 @@ describe('Connector', () => { await waitFor(() => { wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy(); }); act(() => { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx index 4a8b25f4f7b45..5e7972aec9d4b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import React, { memo, useEffect } from 'react'; +import React, { memo, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { UseField, useFormData, FieldHook } from '../../../shared_imports'; +import { UseField, useFormData, FieldHook, useFormContext } from '../../../shared_imports'; import { useConnectors } from '../../containers/configure/use_connectors'; import { ConnectorSelector } from '../connector_selector/form'; -import { SettingFieldsForm } from '../settings/fields_form'; +import { ConnectorFieldsForm } from '../connectors/fields_form'; import { ActionConnector } from '../../containers/types'; import { getConnectorById } from '../configure_cases/utils'; import { FormProps } from './schema'; @@ -20,25 +20,19 @@ interface Props { isLoading: boolean; } -interface SettingsFieldProps { +interface ConnectorsFieldProps { connectors: ActionConnector[]; field: FieldHook; isEdit: boolean; } -const SettingsField = ({ connectors, isEdit, field }: SettingsFieldProps) => { +const ConnectorFields = ({ connectors, isEdit, field }: ConnectorsFieldProps) => { const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); const { setValue } = field; const connector = getConnectorById(connectorId, connectors) ?? null; - useEffect(() => { - if (connectorId) { - setValue(null); - } - }, [setValue, connectorId]); - return ( - { }; const ConnectorComponent: React.FC = ({ isLoading }) => { + const { getFields } = useFormContext(); const { loading: isLoadingConnectors, connectors } = useConnectors(); + const handleConnectorChange = useCallback( + (newConnector) => { + const { fields } = getFields(); + fields.setValue(null); + }, + [getFields] + ); return ( @@ -58,6 +60,7 @@ const ConnectorComponent: React.FC = ({ isLoading }) => { component={ConnectorSelector} componentProps={{ connectors, + handleChange: handleConnectorChange, dataTestSubj: 'caseConnectors', disabled: isLoading || isLoadingConnectors, idAria: 'caseConnectors', @@ -68,7 +71,7 @@ const ConnectorComponent: React.FC = ({ isLoading }) => { { @@ -189,7 +186,7 @@ describe('Create case', () => { connector: { id: 'servicenow-1', name: 'SN', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, persistLoading: false, @@ -237,7 +234,7 @@ describe('Create case', () => { connector: { id: 'not-exist', name: 'SN', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, persistLoading: false, @@ -261,7 +258,7 @@ describe('Create case', () => { wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); await waitFor(() => { expect(postCase).toBeCalledWith(sampleData); - expect(postPushToService).not.toHaveBeenCalled(); + expect(pushCaseToExternalService).not.toHaveBeenCalled(); }); }); }); @@ -283,13 +280,13 @@ describe('Create case', () => { ); fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); await waitFor(() => { wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); }); wrapper @@ -318,17 +315,14 @@ describe('Create case', () => { fields: { issueType: '10007', parent: null, priority: '2' }, }, }); - expect(postPushToService).toHaveBeenCalledWith({ + expect(pushCaseToExternalService).toHaveBeenCalledWith({ caseId: sampleId, - caseServices: {}, connector: { id: 'jira-1', name: 'Jira', type: '.jira', fields: { issueType: '10007', parent: null, priority: '2' }, }, - alerts: {}, - updateCase: noop, }); expect(onFormSubmitSuccess).toHaveBeenCalledWith({ id: sampleId, @@ -353,15 +347,13 @@ describe('Create case', () => { ); fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy(); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); await waitFor(() => { wrapper.update(); - expect( - wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists() - ).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy(); }); act(() => { @@ -390,17 +382,14 @@ describe('Create case', () => { }, }); - expect(postPushToService).toHaveBeenCalledWith({ + expect(pushCaseToExternalService).toHaveBeenCalledWith({ caseId: sampleId, - caseServices: {}, connector: { id: 'resilient-2', name: 'My Connector 2', type: '.resilient', fields: { incidentTypes: ['19'], severityCode: '4' }, }, - alerts: {}, - updateCase: noop, }); expect(onFormSubmitSuccess).toHaveBeenCalledWith({ @@ -426,10 +415,10 @@ describe('Create case', () => { ); fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeFalsy(); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click'); - expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeTruthy(); ['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => { wrapper @@ -453,17 +442,14 @@ describe('Create case', () => { }, }); - expect(postPushToService).toHaveBeenCalledWith({ + expect(pushCaseToExternalService).toHaveBeenCalledWith({ caseId: sampleId, - caseServices: {}, connector: { id: 'servicenow-1', name: 'My Connector', type: '.servicenow', fields: { impact: '2', severity: '2', urgency: '2' }, }, - alerts: {}, - updateCase: noop, }); expect(onFormSubmitSuccess).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index 20ec1e9177cd3..cc38e07cf49e4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -6,7 +6,6 @@ */ import React, { useCallback, useEffect, useMemo } from 'react'; -import { noop } from 'lodash/fp'; import { schema, FormProps } from './schema'; import { Form, useForm } from '../../../shared_imports'; import { @@ -38,7 +37,7 @@ export const FormContext: React.FC = ({ children, onSuccess }) => { const { connectors } = useConnectors(); const { connector: configurationConnector } = useCaseConfigure(); const { postCase } = usePostCase(); - const { postPushToService } = usePostPushToService(); + const { pushCaseToExternalService } = usePostPushToService(); const connectorId = useMemo( () => @@ -67,12 +66,9 @@ export const FormContext: React.FC = ({ children, onSuccess }) => { }); if (updatedCase?.id && dataConnectorId !== 'none') { - await postPushToService({ + await pushCaseToExternalService({ caseId: updatedCase.id, - caseServices: {}, connector: connectorToUpdate, - alerts: {}, - updateCase: noop, }); } @@ -81,7 +77,7 @@ export const FormContext: React.FC = ({ children, onSuccess }) => { } } }, - [connectors, postCase, onSuccess, postPushToService] + [connectors, postCase, onSuccess, pushCaseToExternalService] ); const { form } = useForm({ diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx index f43aecdc123a6..7172d227f492e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx @@ -16,10 +16,10 @@ import { useGetTags } from '../../containers/use_get_tags'; import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types'; -import { useGetSeverity } from '../settings/resilient/use_get_severity'; -import { useGetIssueTypes } from '../settings/jira/use_get_issue_types'; -import { useGetFieldsByIssueType } from '../settings/jira/use_get_fields_by_issue_type'; +import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; +import { useGetSeverity } from '../connectors/resilient/use_get_severity'; +import { useGetIssueTypes } from '../connectors/jira/use_get_issue_types'; +import { useGetFieldsByIssueType } from '../connectors/jira/use_get_fields_by_issue_type'; import { useCaseConfigureResponse } from '../configure_cases/__mock__'; import { useInsertTimeline } from '../use_insert_timeline'; import { @@ -37,12 +37,12 @@ jest.mock('../../containers/api'); jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/configure/use_configure'); -jest.mock('../settings/resilient/use_get_incident_types'); -jest.mock('../settings/resilient/use_get_severity'); -jest.mock('../settings/jira/use_get_issue_types'); -jest.mock('../settings/jira/use_get_fields_by_issue_type'); -jest.mock('../settings/jira/use_get_single_issue'); -jest.mock('../settings/jira/use_get_issues'); +jest.mock('../connectors/resilient/use_get_incident_types'); +jest.mock('../connectors/resilient/use_get_severity'); +jest.mock('../connectors/jira/use_get_issue_types'); +jest.mock('../connectors/jira/use_get_fields_by_issue_type'); +jest.mock('../connectors/jira/use_get_single_issue'); +jest.mock('../connectors/jira/use_get_issues'); jest.mock('../use_insert_timeline'); const useConnectorsMock = useConnectors as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx index 21a87e3a64ac0..34dcacaf42a98 100644 --- a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx @@ -23,8 +23,8 @@ import { noop } from 'lodash/fp'; import { Form, UseField, useForm } from '../../../shared_imports'; import { ConnectorTypeFields } from '../../../../../case/common/api/connectors'; import { ConnectorSelector } from '../connector_selector/form'; -import { ActionConnector } from '../../../../../case/common/api/cases'; -import { SettingFieldsForm } from '../settings/fields_form'; +import { ActionConnector } from '../../../../../case/common/api'; +import { ConnectorFieldsForm } from '../connectors/fields_form'; import { getConnectorById } from '../configure_cases/utils'; import { CaseUserActions } from '../../containers/types'; import { schema } from './schema'; @@ -244,7 +244,7 @@ export const EditConnector = React.memo( - + {(currentConnector == null || currentConnector?.id === 'none') && // Connector is none or not defined. !(currentConnector === null && selectedConnector !== 'none') && // Connector has not been deleted. !editConnector && ( @@ -252,7 +252,7 @@ export const EditConnector = React.memo( {i18n.NO_CONNECTOR} )} - (getJiraCaseSetting()); - this.caseSettingsRegistry.register(getResilientCaseSetting()); - this.caseSettingsRegistry.register(getServiceNowCaseSetting()); - } - - registry(): CaseSettingsRegistry { - return this.caseSettingsRegistry; - } -} - -const caseSettings = new CaseSettings(); - -export const getCaseSettings = (): GetCaseSettingReturn => { - return { - caseSettingsRegistry: caseSettings.registry(), - }; -}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/mock.ts b/x-pack/plugins/security_solution/public/cases/components/settings/mock.ts deleted file mode 100644 index 69f30b488d9a6..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/mock.ts +++ /dev/null @@ -1,21 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const connector = { - id: '123', - name: 'My connector', - actionTypeId: '.jira', - config: {}, - isPreconfigured: false, -}; -export const issues = [ - { id: 'personId', title: 'Person Task', key: 'personKey' }, - { id: 'womanId', title: 'Woman Task', key: 'womanKey' }, - { id: 'manId', title: 'Man Task', key: 'manKey' }, - { id: 'cameraId', title: 'Camera Task', key: 'cameraKey' }, - { id: 'tvId', title: 'TV Task', key: 'tvKey' }, -]; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx deleted file mode 100644 index 161e4d44cd572..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx +++ /dev/null @@ -1,130 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useEffect, useMemo } from 'react'; -import { EuiFormRow, EuiSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as i18n from './translations'; - -import { SettingFieldsProps } from '../types'; -import { ConnectorTypes, ServiceNowFieldsType } from '../../../../../../case/common/api/connectors'; -import { ConnectorCard } from '../card'; - -const selectOptions = [ - { - value: '1', - text: i18n.SEVERITY_HIGH, - }, - { - value: '2', - text: i18n.SEVERITY_MEDIUM, - }, - { - value: '3', - text: i18n.SEVERITY_LOW, - }, -]; - -const ServiceNowSettingFieldsComponent: React.FunctionComponent< - SettingFieldsProps -> = ({ isEdit = true, fields, connector, onChange }) => { - const { severity = null, urgency = null, impact = null } = fields ?? {}; - - const listItems = useMemo( - () => [ - ...(urgency != null && urgency.length > 0 - ? [ - { - title: i18n.URGENCY, - description: selectOptions.find((option) => `${option.value}` === urgency)?.text, - }, - ] - : []), - ...(severity != null && severity.length > 0 - ? [ - { - title: i18n.SEVERITY, - description: selectOptions.find((option) => `${option.value}` === severity)?.text, - }, - ] - : []), - ...(impact != null && impact.length > 0 - ? [ - { - title: i18n.IMPACT, - description: selectOptions.find((option) => `${option.value}` === impact)?.text, - }, - ] - : []), - ], - [urgency, severity, impact] - ); - - // We need to set them up at initialization - useEffect(() => { - onChange({ impact, severity, urgency }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const onChangeCb = useCallback( - (key: keyof ServiceNowFieldsType, value: ServiceNowFieldsType[keyof ServiceNowFieldsType]) => { - onChange({ ...fields, [key]: value }); - }, - [fields, onChange] - ); - - return isEdit ? ( - - - onChangeCb('urgency', e.target.value)} - /> - - - - - - onChangeCb('severity', e.target.value)} - /> - - - - - onChangeCb('impact', e.target.value)} - /> - - - - - ) : ( - - ); -}; - -// eslint-disable-next-line import/no-default-export -export { ServiceNowSettingFieldsComponent as default }; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/index.ts b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/index.ts deleted file mode 100644 index 70d1bf89ce7c8..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/index.ts +++ /dev/null @@ -1,25 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { lazy } from 'react'; - -import { CaseSetting } from '../types'; -import { ServiceNowFieldsType } from '../../../../../../case/common/api/connectors'; -import * as i18n from './translations'; - -export const getCaseSetting = (): CaseSetting => { - return { - id: '.servicenow', - caseSettingFieldsComponent: lazy(() => import('./fields')), - }; -}; - -export const fieldLabels = { - impact: i18n.IMPACT, - severity: i18n.SEVERITY, - urgency: i18n.URGENCY, -}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/translations.ts b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/translations.ts deleted file mode 100644 index 6db239541851e..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/translations.ts +++ /dev/null @@ -1,49 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const SEVERITY_HIGH = i18n.translate( - 'xpack.securitySolution.components.settings.servicenow.severitySelectHighOptionLabel', - { - defaultMessage: 'High', - } -); -export const SEVERITY_MEDIUM = i18n.translate( - 'xpack.securitySolution.components.settings.servicenow.severitySelectMediumOptionLabel', - { - defaultMessage: 'Medium', - } -); - -export const SEVERITY_LOW = i18n.translate( - 'xpack.securitySolution.components.settings.servicenow.severitySelectLowOptionLabel', - { - defaultMessage: 'Low', - } -); - -export const URGENCY = i18n.translate( - 'xpack.securitySolution.components.settings.serviceNow.urgencySelectFieldLabel', - { - defaultMessage: 'Urgency', - } -); - -export const SEVERITY = i18n.translate( - 'xpack.securitySolution.components.settings.serviceNow.severitySelectFieldLabel', - { - defaultMessage: 'Severity', - } -); - -export const IMPACT = i18n.translate( - 'xpack.securitySolution.components.settings.serviceNow.impactSelectFieldLabel', - { - defaultMessage: 'Impact', - } -); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/settings_registry.ts b/x-pack/plugins/security_solution/public/cases/components/settings/settings_registry.ts deleted file mode 100644 index a5580aaf587b2..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/settings_registry.ts +++ /dev/null @@ -1,57 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { CaseSetting, CaseSettingsRegistry } from './types'; - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export const createCaseSettingsRegistry = (): CaseSettingsRegistry => { - const settings: Map> = new Map(); - - const registry: CaseSettingsRegistry = { - has: (id: string) => settings.has(id), - register: (setting: CaseSetting) => { - if (settings.has(setting.id)) { - throw new Error( - i18n.translate( - 'xpack.securitySolution.caseSettingsRegistry.register.duplicateCaseSettingErrorMessage', - { - defaultMessage: 'Object type "{id}" is already registered.', - values: { - id: setting.id, - }, - } - ) - ); - } - - settings.set(setting.id, setting); - }, - get: (id: string): CaseSetting => { - if (!settings.has(id)) { - throw new Error( - i18n.translate( - 'xpack.securitySolution.caseSettingsRegistry.get.missingCaseSettingErrorMessage', - { - defaultMessage: 'Object type "{id}" is not registered.', - values: { - id, - }, - } - ) - ); - } - return settings.get(id)!; - }, - list: () => { - return Array.from(settings).map(([id, setting]) => setting); - }, - }; - - return registry; -}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/types.ts b/x-pack/plugins/security_solution/public/cases/components/settings/types.ts deleted file mode 100644 index 9f212b1999e3d..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/types.ts +++ /dev/null @@ -1,33 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ActionConnector } from '../../../../../case/common/api'; - -import { ConnectorTypeFields } from '../../../../../case/common/api/connectors'; -export type CaseSettingsConnector = ActionConnector; - -export interface CaseSetting { - id: string; - caseSettingFieldsComponent: React.LazyExoticComponent< - React.ComponentType> - > | null; -} - -export interface CaseSettingsRegistry { - has: (id: string) => boolean; - register: (setting: CaseSetting) => void; - get: (id: string) => CaseSetting; - list: () => CaseSetting[]; -} - -export interface SettingFieldsProps { - isEdit?: boolean; - connector: CaseSettingsConnector; - fields: TFields; - onChange: (fields: TFields) => void; -} diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx index 63838b1bc6b8d..b8048afb083f1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx @@ -39,10 +39,10 @@ jest.mock('../../containers/configure/api'); describe('usePushToService', () => { const caseId = '12345'; const updateCase = jest.fn(); - const postPushToService = jest.fn(); + const pushCaseToExternalService = jest.fn(); const mockPostPush = { isLoading: false, - postPushToService, + pushCaseToExternalService, }; const mockConnector = connectorsMock[0]; @@ -61,7 +61,7 @@ describe('usePushToService', () => { connector: { id: mockConnector.id, name: mockConnector.name, - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, caseId, @@ -71,19 +71,6 @@ describe('usePushToService', () => { updateCase, userCanCrud: true, isValidConnector: true, - alerts: { - 'alert-id-1': { - _id: 'alert-id-1', - _index: 'alert-index-1', - '@timestamp': '2020-11-20T15:35:28.373Z', - rule: { - id: 'rule-id-1', - name: 'Awesome rule', - from: 'now-360s', - to: 'now', - }, - }, - }, }; beforeEach(() => { @@ -105,28 +92,13 @@ describe('usePushToService', () => { ); await waitForNextUpdate(); result.current.pushButton.props.children.props.onClick(); - expect(postPushToService).toBeCalledWith({ + expect(pushCaseToExternalService).toBeCalledWith({ caseId, - caseServices, connector: { fields: null, id: 'servicenow-1', name: 'My Connector', - type: ConnectorTypes.servicenow, - }, - updateCase, - alerts: { - 'alert-id-1': { - _id: 'alert-id-1', - _index: 'alert-index-1', - '@timestamp': '2020-11-20T15:35:28.373Z', - rule: { - id: 'rule-id-1', - name: 'Awesome rule', - from: 'now-360s', - to: 'now', - }, - }, + type: ConnectorTypes.serviceNowITSM, }, }); expect(result.current.pushCallouts).toBeNull(); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx index ed03ce36bf26c..21067a3e69969 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx @@ -22,7 +22,6 @@ import { CaseServices } from '../../containers/use_get_case_user_actions'; import { LinkAnchor } from '../../../common/components/links'; import { SecurityPageName } from '../../../app/types'; import { ErrorMessage } from '../callout/types'; -import { Alert } from '../case_view'; export interface UsePushToService { caseId: string; @@ -33,7 +32,6 @@ export interface UsePushToService { updateCase: (newCase: Case) => void; userCanCrud: boolean; isValidConnector: boolean; - alerts: Record; } export interface ReturnUsePushToService { @@ -50,25 +48,25 @@ export const usePushToService = ({ updateCase, userCanCrud, isValidConnector, - alerts, }: UsePushToService): ReturnUsePushToService => { const history = useHistory(); const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case); - const { isLoading, postPushToService } = usePostPushToService(); + const { isLoading, pushCaseToExternalService } = usePostPushToService(); const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); - const handlePushToService = useCallback(() => { + const handlePushToService = useCallback(async () => { if (connector.id != null && connector.id !== 'none') { - postPushToService({ + const theCase = await pushCaseToExternalService({ caseId, - caseServices, connector, - updateCase, - alerts, }); + + if (theCase != null) { + updateCase(theCase); + } } - }, [alerts, caseId, caseServices, connector, postPushToService, updateCase]); + }, [caseId, connector, pushCaseToExternalService, updateCase]); const goToConfigureCases = useCallback( (ev) => { diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index c5d7610aed9ba..4a567a38dc9f2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -24,7 +24,7 @@ import { Case, CaseUserActions } from '../../containers/types'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../../common/lib/kibana'; import { AddComment, AddCommentRefObject } from '../add_comment'; -import { ActionConnector, CommentType } from '../../../../../case/common/api/cases'; +import { ActionConnector, CommentType } from '../../../../../case/common/api'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { parseString } from '../../containers/utils'; import { Alert, OnUpdateFields } from '../case_view'; diff --git a/x-pack/plugins/security_solution/public/cases/containers/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/containers/__mocks__/api.ts index 13b9bc670a4fd..ab761309fa6ad 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/__mocks__/api.ts @@ -25,16 +25,12 @@ import { caseUserActions, pushedCase, respReporters, - serviceConnector, tags, } from '../mock'; import { - CaseExternalServiceRequest, CasePatchRequest, CasePostRequest, CommentRequest, - ServiceConnectorCaseParams, - ServiceConnectorCaseResponse, User, CaseStatuses, } from '../../../../../case/common/api'; @@ -110,15 +106,9 @@ export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promi export const pushCase = async ( caseId: string, - push: CaseExternalServiceRequest, - signal: AbortSignal -): Promise => Promise.resolve(pushedCase); - -export const pushToService = async ( connectorId: string, - casePushParams: ServiceConnectorCaseParams, signal: AbortSignal -): Promise => Promise.resolve(serviceConnector); +): Promise => Promise.resolve(pushedCase); export const getActionLicense = async (signal: AbortSignal): Promise => Promise.resolve(actionLicenses); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index b3e92f24ce2b3..ee63749b49435 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -25,7 +25,6 @@ import { postCase, postComment, pushCase, - pushToService, } from './api'; import { @@ -34,26 +33,20 @@ import { basicCase, allCasesSnake, basicCaseSnake, - actionTypeExecutorResult, pushedCaseSnake, casesStatus, casesSnake, cases, caseUserActions, pushedCase, - pushSnake, reporters, respReporters, - serviceConnector, - casePushParams, tags, caseUserActionsSnake, casesStatusSnake, } from './mock'; import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; -import * as i18n from './translations'; -import { getCaseConfigurePushUrl } from '../../../../case/common/api/helpers'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -84,11 +77,13 @@ describe('Case Configuration API', () => { expect(resp).toEqual(''); }); }); + describe('getActionLicense', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(actionLicenses); }); + test('check url, method, signal', async () => { await getActionLicense(abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`/api/actions/list_action_types`, { @@ -102,6 +97,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(actionLicenses); }); }); + describe('getCase', () => { beforeEach(() => { fetchMock.mockClear(); @@ -123,6 +119,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(basicCase); }); }); + describe('getCases', () => { beforeEach(() => { fetchMock.mockClear(); @@ -145,6 +142,7 @@ describe('Case Configuration API', () => { signal: abortCtrl.signal, }); }); + test('correctly applies filters', async () => { await getCases({ filterOptions: { @@ -169,6 +167,7 @@ describe('Case Configuration API', () => { signal: abortCtrl.signal, }); }); + test('tags with weird chars get handled gracefully', async () => { const weirdTags: string[] = ['(', '"double"']; @@ -205,6 +204,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual({ ...allCases }); }); }); + describe('getCasesStatus', () => { beforeEach(() => { fetchMock.mockClear(); @@ -223,6 +223,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(casesStatus); }); }); + describe('getCaseUserActions', () => { beforeEach(() => { fetchMock.mockClear(); @@ -242,6 +243,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(caseUserActions); }); }); + describe('getReporters', () => { beforeEach(() => { fetchMock.mockClear(); @@ -261,6 +263,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(respReporters); }); }); + describe('getTags', () => { beforeEach(() => { fetchMock.mockClear(); @@ -280,6 +283,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(tags); }); }); + describe('patchCase', () => { beforeEach(() => { fetchMock.mockClear(); @@ -307,6 +311,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual({ ...[basicCase] }); }); }); + describe('patchCasesStatus', () => { beforeEach(() => { fetchMock.mockClear(); @@ -334,6 +339,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual({ ...cases }); }); }); + describe('patchComment', () => { beforeEach(() => { fetchMock.mockClear(); @@ -371,6 +377,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(basicCase); }); }); + describe('postCase', () => { beforeEach(() => { fetchMock.mockClear(); @@ -405,6 +412,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(basicCase); }); }); + describe('postComment', () => { beforeEach(() => { fetchMock.mockClear(); @@ -429,88 +437,30 @@ describe('Case Configuration API', () => { expect(resp).toEqual(basicCase); }); }); + describe('pushCase', () => { + const connectorId = 'connectorId'; + beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(pushedCaseSnake); }); test('check url, method, signal', async () => { - await pushCase(basicCase.id, pushSnake, abortCtrl.signal); - expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/_push`, { - method: 'POST', - body: JSON.stringify(pushSnake), - signal: abortCtrl.signal, - }); + await pushCase(basicCase.id, connectorId, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith( + `${CASES_URL}/${basicCase.id}/connector/${connectorId}/_push`, + { + method: 'POST', + body: JSON.stringify({}), + signal: abortCtrl.signal, + } + ); }); test('happy path', async () => { - const resp = await pushCase(basicCase.id, pushSnake, abortCtrl.signal); + const resp = await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(resp).toEqual(pushedCase); }); }); - describe('pushToService', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(actionTypeExecutorResult); - }); - const connectorId = 'connectorId'; - test('check url, method, signal', async () => { - await pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal); - expect(fetchMock).toHaveBeenCalledWith(`${getCaseConfigurePushUrl(connectorId)}`, { - method: 'POST', - body: JSON.stringify({ - connector_type: ConnectorTypes.jira, - params: casePushParams, - }), - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const resp = await pushToService( - connectorId, - ConnectorTypes.jira, - casePushParams, - abortCtrl.signal - ); - expect(resp).toEqual(serviceConnector); - }); - - test('unhappy path - serviceMessage', async () => { - const theError = 'the error'; - fetchMock.mockResolvedValue({ - ...actionTypeExecutorResult, - status: 'error', - serviceMessage: theError, - message: 'not it', - }); - await expect( - pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal) - ).rejects.toMatchObject({ message: theError }); - }); - - test('unhappy path - message', async () => { - const theError = 'the error'; - fetchMock.mockResolvedValue({ - ...actionTypeExecutorResult, - status: 'error', - message: theError, - }); - await expect( - pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal) - ).rejects.toMatchObject({ message: theError }); - }); - - test('unhappy path - no message', async () => { - const theError = i18n.ERROR_PUSH_TO_SERVICE; - fetchMock.mockResolvedValue({ - ...actionTypeExecutorResult, - status: 'error', - }); - await expect( - pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal) - ).rejects.toMatchObject({ message: theError }); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 22e6c92da8ceb..00a45aadd2ae0 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -6,7 +6,6 @@ */ import { - CaseExternalServiceRequest, CasePatchRequest, CasePostRequest, CaseResponse, @@ -17,8 +16,6 @@ import { CaseUserActionsResponse, CommentRequest, CommentType, - ServiceConnectorCaseParams, - ServiceConnectorCaseResponse, User, } from '../../../../case/common/api'; @@ -32,7 +29,7 @@ import { import { getCaseCommentsUrl, - getCaseConfigurePushUrl, + getCasePushUrl, getCaseDetailsUrl, getCaseUserActionUrl, } from '../../../../case/common/api/helpers'; @@ -59,10 +56,8 @@ import { decodeCasesFindResponse, decodeCasesStatusResponse, decodeCaseUserActionsResponse, - decodeServiceConnectorCaseResponse, } from './utils'; -import * as i18n from './translations'; -import { ActionTypeExecutorResult } from '../../../../actions/common'; + export const getCase = async ( caseId: string, includeComments: boolean = true, @@ -231,41 +226,19 @@ export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promi export const pushCase = async ( caseId: string, - push: CaseExternalServiceRequest, + connectorId: string, signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - `${getCaseDetailsUrl(caseId)}/_push`, + getCasePushUrl(caseId, connectorId), { method: 'POST', - body: JSON.stringify(push), + body: JSON.stringify({}), signal, } ); - return convertToCamelCase(decodeCaseResponse(response)); -}; -export const pushToService = async ( - connectorId: string, - connectorType: string, - casePushParams: ServiceConnectorCaseParams, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch< - ActionTypeExecutorResult> - >(`${getCaseConfigurePushUrl(connectorId)}`, { - method: 'POST', - body: JSON.stringify({ - connector_type: connectorType, - params: casePushParams, - }), - signal, - }); - - if (response.status === 'error') { - throw new Error(response.serviceMessage ?? response.message ?? i18n.ERROR_PUSH_TO_SERVICE); - } - return decodeServiceConnectorCaseResponse(response.data); + return convertToCamelCase(decodeCaseResponse(response)); }; export const getActionLicense = async (signal: AbortSignal): Promise => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 06983a92b9ea1..444a87a57d251 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -9,7 +9,6 @@ import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } import { CommentResponse, - ServiceConnectorCaseResponse, CaseStatuses, UserAction, UserActionField, @@ -29,17 +28,13 @@ const basicCommentId = 'basic-comment-id'; const basicCreatedAt = '2020-02-19T23:06:33.798Z'; const basicUpdatedAt = '2020-02-20T15:02:57.995Z'; const laterTime = '2020-02-28T15:02:57.995Z'; + export const elasticUser = { fullName: 'Leslie Knope', username: 'lknope', email: 'leslie.knope@elastic.co', }; -export const serviceConnectorUser = { - fullName: 'Leslie Knope', - username: 'lknope', -}; - export const tags: string[] = ['coke', 'pepsi']; export const basicComment: Comment = { @@ -136,19 +131,6 @@ export const pushedCase: Case = { externalService: basicPush, }; -export const serviceConnector: ServiceConnectorCaseResponse = { - title: '123', - id: '444', - pushedDate: basicUpdatedAt, - url: 'connector.com', - comments: [ - { - commentId: basicCommentId, - pushedDate: basicUpdatedAt, - }, - ], -}; - const basicAction = { actionAt: basicCreatedAt, actionBy: elasticUser, @@ -158,25 +140,6 @@ const basicAction = { commentId: null, }; -export const casePushParams = { - savedObjectId: basicCaseId, - createdAt: basicCreatedAt, - createdBy: elasticUser, - externalId: null, - title: 'what a cool value', - commentId: null, - updatedAt: basicCreatedAt, - updatedBy: elasticUser, - description: 'nice', - comments: null, -}; - -export const actionTypeExecutorResult = { - actionId: 'string', - status: 'ok', - data: serviceConnector, -}; - export const cases: Case[] = [ basicCase, { ...pushedCase, id: '1', totalComment: 0, comments: [] }, @@ -192,6 +155,7 @@ export const allCases: AllCases = { total: 10, ...casesStatus, }; + export const actionLicenses: ActionLicense[] = [ { id: '.servicenow', @@ -215,6 +179,7 @@ export const elasticUserSnake = { username: 'lknope', email: 'leslie.knope@elastic.co', }; + export const basicCommentSnake: CommentResponse = { comment: 'Solve this fast!', type: CommentType.user, @@ -260,11 +225,13 @@ export const pushSnake = { external_title: 'external title', external_url: 'basicPush.com', }; + export const basicPushSnake = { ...pushSnake, pushed_at: basicUpdatedAt, pushed_by: elasticUserSnake, }; + export const pushedCaseSnake = { ...basicCaseSnake, external_service: basicPushSnake, diff --git a/x-pack/plugins/security_solution/public/cases/containers/translations.ts b/x-pack/plugins/security_solution/public/cases/containers/translations.ts index 9525d125435e7..75939b46b1f77 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/translations.ts @@ -62,13 +62,6 @@ export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = (serviceName: string) => defaultMessage: 'Successfully sent to { serviceName }', }); -export const ERROR_PUSH_TO_SERVICE = i18n.translate( - 'xpack.securitySolution.case.configure.errorPushingToService', - { - defaultMessage: 'Error pushing to service', - } -); - export const ERROR_GET_FIELDS = i18n.translate( 'xpack.securitySolution.case.configure.errorGetFields', { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx index 8845e285ee910..5f09ac404ca64 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx @@ -6,112 +6,22 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { - formatServiceRequestData, - usePostPushToService, - UsePostPushToService, -} from './use_post_push_to_service'; -import { - basicCase, - basicComment, - basicPush, - pushedCase, - serviceConnector, - serviceConnectorUser, -} from './mock'; +import { usePostPushToService, UsePostPushToService } from './use_post_push_to_service'; +import { pushedCase } from './mock'; import * as api from './api'; -import { CaseServices } from './use_get_case_user_actions'; -import { CaseConnector, ConnectorTypes, CommentType } from '../../../../case/common/api'; -import moment from 'moment'; +import { CaseConnector, ConnectorTypes } from '../../../../case/common/api'; + jest.mock('./api'); -jest.mock('../../common/components/link_to', () => { - const originalModule = jest.requireActual('../../common/components/link_to'); - return { - ...originalModule, - getTimelineTabsUrl: jest.fn(), - useFormatUrl: jest.fn().mockReturnValue({ formatUrl: jest.fn(), search: 'urlSearch' }), - }; -}); + describe('usePostPushToService', () => { const abortCtrl = new AbortController(); - const updateCase = jest.fn(); - const formatUrl = jest.fn(); - - const samplePush = { - caseId: pushedCase.id, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 1, - lastPushIndex: 1, - commentsToUpdate: [basicComment.id], - hasDataToPush: false, - }, - }, - connector: { - id: '123', - name: 'connector name', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'Low', parent: null }, - } as CaseConnector, - updateCase, - alerts: { - 'alert-id-1': { - _id: 'alert-id-1', - _index: 'alert-index-1', - '@timestamp': '2020-11-20T15:35:28.373Z', - rule: { - id: 'rule-id-1', - name: 'Awesome rule', - from: 'now-360s', - to: 'now', - }, - }, - }, - }; - - const sampleServiceRequestData = { - savedObjectId: pushedCase.id, - createdAt: pushedCase.createdAt, - createdBy: serviceConnectorUser, - comments: [ - { - commentId: basicComment.id, - comment: basicComment.type === CommentType.user ? basicComment.comment : '', - createdAt: basicComment.createdAt, - createdBy: serviceConnectorUser, - updatedAt: null, - updatedBy: null, - }, - ], - externalId: basicPush.externalId, - description: pushedCase.description, - title: pushedCase.title, - updatedAt: pushedCase.updatedAt, - updatedBy: serviceConnectorUser, - issueType: 'Task', - parent: null, - priority: 'Low', - }; - - const sampleCaseServices = { - '123': { - ...basicPush, - firstPushIndex: 1, - lastPushIndex: 1, - commentsToUpdate: [basicComment.id], - hasDataToPush: true, - }, - '456': { - ...basicPush, - connectorId: '456', - externalId: 'other_external_id', - firstPushIndex: 4, - commentsToUpdate: [basicComment.id], - lastPushIndex: 6, - hasDataToPush: false, - }, - }; + const connector = { + id: '123', + name: 'connector name', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'Low', parent: null }, + } as CaseConnector; + const caseId = pushedCase.id; it('init', async () => { await act(async () => { @@ -120,98 +30,24 @@ describe('usePostPushToService', () => { ); await waitForNextUpdate(); expect(result.current).toEqual({ - serviceData: null, - pushedCaseData: null, isLoading: false, isError: false, - postPushToService: result.current.postPushToService, + pushCaseToExternalService: result.current.pushCaseToExternalService, }); }); }); it('calls pushCase with correct arguments', async () => { - const spyOnPushCase = jest.spyOn(api, 'pushCase'); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePostPushToService() - ); - await waitForNextUpdate(); - result.current.postPushToService(samplePush); - await waitForNextUpdate(); - expect(spyOnPushCase).toBeCalledWith( - samplePush.caseId, - { - connector_id: samplePush.connector.id, - connector_name: samplePush.connector.name, - external_id: serviceConnector.id, - external_title: serviceConnector.title, - external_url: serviceConnector.url, - }, - abortCtrl.signal - ); - }); - }); - - it('calls pushToService with correct arguments', async () => { - const spyOnPushToService = jest.spyOn(api, 'pushToService'); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePostPushToService() - ); - await waitForNextUpdate(); - result.current.postPushToService(samplePush); - await waitForNextUpdate(); - expect(spyOnPushToService).toBeCalledWith( - samplePush.connector.id, - samplePush.connector.type, - formatServiceRequestData({ - myCase: basicCase, - connector: samplePush.connector, - caseServices: sampleCaseServices as CaseServices, - alerts: samplePush.alerts, - formatUrl, - }), - abortCtrl.signal - ); - }); - }); - - it('calls pushToService with correct arguments when no push history', async () => { - const samplePush2 = { - caseId: pushedCase.id, - caseServices: {}, - connector: { - name: 'connector name', - id: 'none', - type: ConnectorTypes.none, - fields: null, - }, - alerts: samplePush.alerts, - updateCase, - }; - const spyOnPushToService = jest.spyOn(api, 'pushToService'); + const spyOnPushToService = jest.spyOn(api, 'pushCase'); await act(async () => { const { result, waitForNextUpdate } = renderHook(() => usePostPushToService() ); await waitForNextUpdate(); - result.current.postPushToService(samplePush2); + result.current.pushCaseToExternalService({ caseId, connector }); await waitForNextUpdate(); - expect(spyOnPushToService).toBeCalledWith( - samplePush2.connector.id, - samplePush2.connector.type, - formatServiceRequestData({ - myCase: basicCase, - connector: samplePush2.connector, - caseServices: {}, - alerts: samplePush.alerts, - formatUrl, - }), - abortCtrl.signal - ); + expect(spyOnPushToService).toBeCalledWith(caseId, connector.id, abortCtrl.signal); }); }); @@ -221,120 +57,29 @@ describe('usePostPushToService', () => { usePostPushToService() ); await waitForNextUpdate(); - result.current.postPushToService(samplePush); + result.current.pushCaseToExternalService({ caseId, connector }); await waitForNextUpdate(); expect(result.current).toEqual({ - serviceData: serviceConnector, - pushedCaseData: pushedCase, isLoading: false, isError: false, - postPushToService: result.current.postPushToService, + pushCaseToExternalService: result.current.pushCaseToExternalService, }); }); }); - it('set isLoading to true when deleting cases', async () => { + it('set isLoading to true when pushing case', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => usePostPushToService() ); await waitForNextUpdate(); - result.current.postPushToService(samplePush); + result.current.pushCaseToExternalService({ caseId, connector }); expect(result.current.isLoading).toBe(true); }); }); - it('formatServiceRequestData - current connector', () => { - const caseServices = sampleCaseServices; - const result = formatServiceRequestData({ - myCase: pushedCase, - connector: samplePush.connector, - caseServices, - alerts: samplePush.alerts, - formatUrl, - }); - expect(result).toEqual(sampleServiceRequestData); - }); - - it('formatServiceRequestData - connector with history', () => { - const caseServices = sampleCaseServices; - const connector = { - id: '456', - name: 'connector 2', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: 'RJ-01' }, - }; - const result = formatServiceRequestData({ - myCase: pushedCase, - connector: connector as CaseConnector, - caseServices, - alerts: samplePush.alerts, - formatUrl, - }); - expect(result).toEqual({ - ...sampleServiceRequestData, - ...connector.fields, - externalId: 'other_external_id', - }); - }); - - it('formatServiceRequestData - new connector', () => { - const caseServices = { - '123': sampleCaseServices['123'], - }; - - const connector = { - id: '456', - name: 'connector 2', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }; - - const result = formatServiceRequestData({ - myCase: pushedCase, - connector: connector as CaseConnector, - caseServices, - alerts: samplePush.alerts, - formatUrl, - }); - - expect(result).toEqual({ - ...sampleServiceRequestData, - ...connector.fields, - externalId: null, - }); - }); - - it('formatServiceRequestData - Alert comment content', () => { - const mockDuration = moment.duration(1); - jest.spyOn(moment, 'duration').mockReturnValue(mockDuration); - formatUrl.mockReturnValue('https://app.com/detections'); - const caseServices = sampleCaseServices; - const result = formatServiceRequestData({ - myCase: { - ...pushedCase, - comments: [ - { - ...pushedCase.comments[0], - type: CommentType.alert, - alertId: 'alert-id-1', - index: 'alert-index-1', - }, - ], - }, - connector: samplePush.connector, - caseServices, - alerts: samplePush.alerts, - formatUrl, - }); - - expect(result.comments![0].comment).toEqual( - '[Alert](https://app.com/detections?filters=!((%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,key:_id,negate:!f,params:(query:alert-id-1),type:phrase),query:(match:(_id:(query:alert-id-1,type:phrase)))))&sourcerer=(default:!())&timerange=(global:(linkTo:!(timeline),timerange:(from:%272020-11-20T15:35:28.372Z%27,kind:absolute,to:%272020-11-20T15:35:28.373Z%27)),timeline:(linkTo:!(global),timerange:(from:%272020-11-20T15:35:28.372Z%27,kind:absolute,to:%272020-11-20T15:35:28.373Z%27)))) added to case.' - ); - }); - it('unhappy path', async () => { - const spyOnPushToService = jest.spyOn(api, 'pushToService'); + const spyOnPushToService = jest.spyOn(api, 'pushCase'); spyOnPushToService.mockImplementation(() => { throw new Error('Something went wrong'); }); @@ -344,15 +89,12 @@ describe('usePostPushToService', () => { usePostPushToService() ); await waitForNextUpdate(); - result.current.postPushToService(samplePush); - await waitForNextUpdate(); + result.current.pushCaseToExternalService({ caseId, connector }); expect(result.current).toEqual({ - serviceData: null, - pushedCaseData: null, isLoading: false, isError: true, - postPushToService: result.current.postPushToService, + pushCaseToExternalService: result.current.pushCaseToExternalService, }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx index c5b4f52e73125..03d881d7934e9 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx @@ -5,41 +5,23 @@ * 2.0. */ -import { useReducer, useCallback } from 'react'; -import moment from 'moment'; -import dateMath from '@elastic/datemath'; - -import { - ServiceConnectorCaseResponse, - ServiceConnectorCaseParams, - CaseConnector, - CommentType, -} from '../../../../case/common/api'; -import { SecurityPageName } from '../../app/types'; -import { useFormatUrl, FormatUrl, getRuleDetailsUrl } from '../../common/components/link_to'; +import { useReducer, useCallback, useRef, useEffect } from 'react'; +import { CaseConnector } from '../../../../case/common/api'; import { errorToToaster, useStateToaster, displaySuccessToast, } from '../../common/components/toasters'; -import { Alert } from '../components/case_view'; -import { getCase, pushToService, pushCase } from './api'; +import { pushCase } from './api'; import * as i18n from './translations'; -import { Case, Comment } from './types'; -import { CaseServices } from './use_get_case_user_actions'; +import { Case } from './types'; interface PushToServiceState { - serviceData: ServiceConnectorCaseResponse | null; - pushedCaseData: Case | null; isLoading: boolean; isError: boolean; } -type Action = - | { type: 'FETCH_INIT' } - | { type: 'FETCH_SUCCESS_PUSH_SERVICE'; payload: ServiceConnectorCaseResponse | null } - | { type: 'FETCH_SUCCESS_PUSH_CASE'; payload: Case | null } - | { type: 'FETCH_FAILURE' }; +type Action = { type: 'FETCH_INIT' } | { type: 'FETCH_SUCCESS' } | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: PushToServiceState, action: Action): PushToServiceState => { switch (action.type) { @@ -49,19 +31,11 @@ const dataFetchReducer = (state: PushToServiceState, action: Action): PushToServ isLoading: true, isError: false, }; - case 'FETCH_SUCCESS_PUSH_SERVICE': - return { - ...state, - isLoading: false, - isError: false, - serviceData: action.payload ?? null, - }; - case 'FETCH_SUCCESS_PUSH_CASE': + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - pushedCaseData: action.payload ?? null, }; case 'FETCH_FAILURE': return { @@ -77,72 +51,45 @@ const dataFetchReducer = (state: PushToServiceState, action: Action): PushToServ interface PushToServiceRequest { caseId: string; connector: CaseConnector; - caseServices: CaseServices; - alerts: Record; - updateCase: (newCase: Case) => void; } export interface UsePostPushToService extends PushToServiceState { - postPushToService: ({ + pushCaseToExternalService: ({ caseId, - caseServices, connector, - alerts, - updateCase, - }: PushToServiceRequest) => void; + }: PushToServiceRequest) => Promise; } export const usePostPushToService = (): UsePostPushToService => { const [state, dispatch] = useReducer(dataFetchReducer, { - serviceData: null, - pushedCaseData: null, isLoading: false, isError: false, }); const [, dispatchToaster] = useStateToaster(); - const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const cancel = useRef(false); + const abortCtrl = useRef(new AbortController()); - const postPushToService = useCallback( - async ({ caseId, caseServices, connector, alerts, updateCase }: PushToServiceRequest) => { - let cancel = false; - const abortCtrl = new AbortController(); + const pushCaseToExternalService = useCallback( + async ({ caseId, connector }: PushToServiceRequest) => { try { dispatch({ type: 'FETCH_INIT' }); - const casePushData = await getCase(caseId, true, abortCtrl.signal); - const responseService = await pushToService( - connector.id, - connector.type, - formatServiceRequestData({ - myCase: casePushData, - connector, - caseServices, - alerts, - formatUrl, - }), - abortCtrl.signal - ); - const responseCase = await pushCase( - caseId, - { - connector_id: connector.id, - connector_name: connector.name, - external_id: responseService.id, - external_title: responseService.title, - external_url: responseService.url, - }, - abortCtrl.signal - ); - if (!cancel) { - dispatch({ type: 'FETCH_SUCCESS_PUSH_SERVICE', payload: responseService }); - dispatch({ type: 'FETCH_SUCCESS_PUSH_CASE', payload: responseCase }); - updateCase(responseCase); + abortCtrl.current.abort(); + cancel.current = false; + abortCtrl.current = new AbortController(); + + const response = await pushCase(caseId, connector.id, abortCtrl.current.signal); + + if (!cancel.current) { + dispatch({ type: 'FETCH_SUCCESS' }); displaySuccessToast( i18n.SUCCESS_SEND_TO_EXTERNAL_SERVICE(connector.name), dispatchToaster ); } + + return response; } catch (error) { - if (!cancel) { + if (!cancel.current) { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, @@ -151,123 +98,17 @@ export const usePostPushToService = (): UsePostPushToService => { dispatch({ type: 'FETCH_FAILURE' }); } } - return () => { - cancel = true; - abortCtrl.abort(); - }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [] ); - return { ...state, postPushToService }; -}; - -export const determineToAndFrom = (alert: Alert) => { - const ellapsedTimeRule = moment.duration( - moment().diff(dateMath.parse(alert.rule?.from != null ? alert.rule.from : 'now-0s')) - ); + useEffect(() => { + return () => { + abortCtrl.current.abort(); + cancel.current = true; + }; + }, []); - const from = moment(alert['@timestamp'] ?? new Date()) - .subtract(ellapsedTimeRule) - .toISOString(); - const to = moment(alert['@timestamp'] ?? new Date()).toISOString(); - - return { to, from }; -}; - -const getAlertFilterUrl = (alert: Alert): string => { - const { to, from } = determineToAndFrom(alert); - return `?filters=!((%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,key:_id,negate:!f,params:(query:${alert._id}),type:phrase),query:(match:(_id:(query:${alert._id},type:phrase)))))&sourcerer=(default:!())&timerange=(global:(linkTo:!(timeline),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)),timeline:(linkTo:!(global),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)))`; -}; - -const getCommentContent = ( - comment: Comment, - alerts: Record, - formatUrl: FormatUrl -): string => { - if (comment.type === CommentType.user) { - return comment.comment; - } else if (comment.type === CommentType.alert) { - const alert = alerts[comment.alertId]; - const ruleDetailsLink = formatUrl(getRuleDetailsUrl(alert.rule.id), { - absolute: true, - skipSearch: true, - }); - - return `[${i18n.ALERT}](${ruleDetailsLink}${getAlertFilterUrl(alert)}) ${ - i18n.ALERT_ADDED_TO_CASE - }.`; - } - - return ''; -}; - -export const formatServiceRequestData = ({ - myCase, - connector, - caseServices, - alerts, - formatUrl, -}: { - myCase: Case; - connector: CaseConnector; - caseServices: CaseServices; - alerts: Record; - formatUrl: FormatUrl; -}): ServiceConnectorCaseParams => { - const { - id: caseId, - createdAt, - createdBy, - comments, - description, - title, - updatedAt, - updatedBy, - } = myCase; - const actualExternalService = caseServices[connector.id] ?? null; - - return { - savedObjectId: caseId, - createdAt, - createdBy: { - fullName: createdBy.fullName ?? null, - username: createdBy?.username ?? '', - }, - comments: comments - .filter( - (c) => - actualExternalService == null || actualExternalService.commentsToUpdate.includes(c.id) - ) - .map((c) => ({ - commentId: c.id, - comment: getCommentContent(c, alerts, formatUrl), - createdAt: c.createdAt, - createdBy: { - fullName: c.createdBy.fullName ?? null, - username: c.createdBy.username ?? '', - }, - updatedAt: c.updatedAt, - updatedBy: - c.updatedBy != null - ? { - fullName: c.updatedBy.fullName ?? null, - username: c.updatedBy.username ?? '', - } - : null, - })), - description, - externalId: actualExternalService?.externalId ?? null, - title, - ...(connector.fields ?? {}), - updatedAt, - updatedBy: - updatedBy != null - ? { - fullName: updatedBy.fullName ?? null, - username: updatedBy.username ?? '', - } - : null, - }; + return { ...state, pushCaseToExternalService }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/utils.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.ts index 4311390ae9b49..297c7e35981ac 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/utils.ts @@ -26,8 +26,6 @@ import { CaseConfigureResponseRt, CaseUserActionsResponse, CaseUserActionsResponseRt, - ServiceConnectorCaseResponseRt, - ServiceConnectorCaseResponse, CommentType, CasePatchRequest, } from '../../../../case/common/api'; @@ -107,12 +105,6 @@ export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsR fold(throwErrors(createToasterPlainError), identity) ); -export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnectorCaseResponse) => - pipe( - ServiceConnectorCaseResponseRt.decode(respPushCase), - fold(throwErrors(createToasterPlainError), identity) - ); - export const valueToUpdateIsSettings = ( key: UpdateByKey['updateKey'], value: UpdateByKey['updateValue'] diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c264e807ea234..5e61f58e7afac 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17352,7 +17352,6 @@ "xpack.securitySolution.case.components.connectors.case.optionAddToExistingCase": "既存のケースに追加", "xpack.securitySolution.case.components.connectors.case.selectMessageText": "ケースを作成または更新します。", "xpack.securitySolution.case.configure.errorGetFields": "サービスからのフィールドの取得中にエラーが発生しました", - "xpack.securitySolution.case.configure.errorPushingToService": "サービスへのプッシュエラー", "xpack.securitySolution.case.configure.successSaveToast": "保存された外部接続設定", "xpack.securitySolution.case.configureCases.addNewConnector": "新しいコネクターを追加", "xpack.securitySolution.case.configureCases.blankMappings": "1 つ以上のフィールドを { connectorName } にマッピングする必要があります", @@ -17401,14 +17400,6 @@ "xpack.securitySolution.case.pageTitle": "ケース", "xpack.securitySolution.case.readOnlySavedObjectDescription": "ケースを表示する権限のみが付与されています。ケースを開いて更新する必要がある場合は、Kibana管理者に連絡してください。", "xpack.securitySolution.case.readOnlySavedObjectTitle": "新しいケースを開いたり、既存のケースを更新したりすることはできません", - "xpack.securitySolution.case.settings.jira.issueTypesSelectFieldLabel": "問題タイプ", - "xpack.securitySolution.case.settings.jira.parentIssueSearchLabel": "親問題", - "xpack.securitySolution.case.settings.jira.prioritySelectFieldLabel": "優先度", - "xpack.securitySolution.case.settings.resilient.incidentTypesLabel": "インシデントタイプ", - "xpack.securitySolution.case.settings.resilient.incidentTypesPlaceholder": "タイプを選択", - "xpack.securitySolution.case.settings.resilient.severityLabel": "深刻度", - "xpack.securitySolution.case.settings.resilient.unableToGetIncidentTypesMessage": "インシデントタイプを取得できません", - "xpack.securitySolution.case.settings.resilient.unableToGetSeverityMessage": "深刻度を取得できません", "xpack.securitySolution.case.settings.syncAlertsSwitchLabelOff": "オフ", "xpack.securitySolution.case.settings.syncAlertsSwitchLabelOn": "オン", "xpack.securitySolution.case.status.closed": "終了", @@ -17421,8 +17412,6 @@ "xpack.securitySolution.case.timeline.actions.addToCaseAriaLabel": "アラートをケースに関連付ける", "xpack.securitySolution.case.timeline.actions.addToCaseTooltip": "ケースに追加", "xpack.securitySolution.case.timeline.actions.caseCreatedSuccessToast": "アラートが「{title}」に追加されました", - "xpack.securitySolution.caseSettingsRegistry.get.missingCaseSettingErrorMessage": "オブジェクトタイプ「{id}」は登録されていません。", - "xpack.securitySolution.caseSettingsRegistry.register.duplicateCaseSettingErrorMessage": "オブジェクトタイプ「{id}」は既に登録されています。", "xpack.securitySolution.certificate.fingerprint.clientCertLabel": "クライアント証明書", "xpack.securitySolution.certificate.fingerprint.serverCertLabel": "サーバー証明書", "xpack.securitySolution.chart.allOthersGroupingLabel": "その他すべて", @@ -17510,19 +17499,6 @@ "xpack.securitySolution.components.mlPopup.upgradeButtonLabel": "サブスクリプションオプション", "xpack.securitySolution.components.mlPopup.upgradeDescription": "SIEMの異常検出機能にアクセスするには、ライセンスをプラチナに更新するか、30日間の無料トライアルを開始するか、AWS、GCP、またはAzureで{cloudLink}にサインアップしてください。その後、機械学習ジョブを実行して異常を表示できます。", "xpack.securitySolution.components.mlPopup.upgradeTitle": "Elastic Platinum へのアップグレード", - "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel": "入力して検索", - "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder": "入力して検索", - "xpack.securitySolution.components.settings.jira.searchIssuesLoading": "読み込み中…", - "xpack.securitySolution.components.settings.jira.unableToGetFieldsMessage": "フィールドを取得できません", - "xpack.securitySolution.components.settings.jira.unableToGetIssueMessage": "ID {id}の問題を取得できません", - "xpack.securitySolution.components.settings.jira.unableToGetIssuesMessage": "問題を取得できません", - "xpack.securitySolution.components.settings.jira.unableToGetIssueTypesMessage": "問題タイプを取得できません", - "xpack.securitySolution.components.settings.serviceNow.impactSelectFieldLabel": "インパクト", - "xpack.securitySolution.components.settings.serviceNow.severitySelectFieldLabel": "深刻度", - "xpack.securitySolution.components.settings.servicenow.severitySelectHighOptionLabel": "高", - "xpack.securitySolution.components.settings.servicenow.severitySelectLowOptionLabel": "低", - "xpack.securitySolution.components.settings.servicenow.severitySelectMediumOptionLabel": "中", - "xpack.securitySolution.components.settings.serviceNow.urgencySelectFieldLabel": "緊急", "xpack.securitySolution.components.stepDefineRule.ruleTypeField.subscriptionsLink": "プラチナサブスクリプション", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "異常データをクエリできませんでした", "xpack.securitySolution.containers.anomalies.stackByJobId": "ジョブ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a2fe8e81e4635..14e26395ad3ce 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17396,7 +17396,6 @@ "xpack.securitySolution.case.components.connectors.case.optionAddToExistingCase": "添加到现有案例", "xpack.securitySolution.case.components.connectors.case.selectMessageText": "创建或更新案例。", "xpack.securitySolution.case.configure.errorGetFields": "从服务中获取字段时出错", - "xpack.securitySolution.case.configure.errorPushingToService": "推送到服务时出错", "xpack.securitySolution.case.configure.successSaveToast": "已保存外部连接设置", "xpack.securitySolution.case.configureCases.addNewConnector": "添加新连接器", "xpack.securitySolution.case.configureCases.blankMappings": "至少一个字段需映射到 { connectorName }", @@ -17445,14 +17444,6 @@ "xpack.securitySolution.case.pageTitle": "案例", "xpack.securitySolution.case.readOnlySavedObjectDescription": "您仅有权查看案例。如果需要创建和更新案例,请联系您的 Kibana 管理员。", "xpack.securitySolution.case.readOnlySavedObjectTitle": "您无法创建新案例或更新现有案例", - "xpack.securitySolution.case.settings.jira.issueTypesSelectFieldLabel": "问题类型", - "xpack.securitySolution.case.settings.jira.parentIssueSearchLabel": "父问题", - "xpack.securitySolution.case.settings.jira.prioritySelectFieldLabel": "优先级", - "xpack.securitySolution.case.settings.resilient.incidentTypesLabel": "事件类型", - "xpack.securitySolution.case.settings.resilient.incidentTypesPlaceholder": "选择类型", - "xpack.securitySolution.case.settings.resilient.severityLabel": "严重性", - "xpack.securitySolution.case.settings.resilient.unableToGetIncidentTypesMessage": "无法获取事件类型", - "xpack.securitySolution.case.settings.resilient.unableToGetSeverityMessage": "无法获取严重性", "xpack.securitySolution.case.settings.syncAlertsSwitchLabelOff": "关闭", "xpack.securitySolution.case.settings.syncAlertsSwitchLabelOn": "开启", "xpack.securitySolution.case.status.closed": "已关闭", @@ -17465,8 +17456,6 @@ "xpack.securitySolution.case.timeline.actions.addToCaseAriaLabel": "将告警附加到案例", "xpack.securitySolution.case.timeline.actions.addToCaseTooltip": "添加到案例", "xpack.securitySolution.case.timeline.actions.caseCreatedSuccessToast": "告警已添加到“{title}”", - "xpack.securitySolution.caseSettingsRegistry.get.missingCaseSettingErrorMessage": "对象类型“{id}”未注册。", - "xpack.securitySolution.caseSettingsRegistry.register.duplicateCaseSettingErrorMessage": "已注册对象类型“{id}”。", "xpack.securitySolution.certificate.fingerprint.clientCertLabel": "客户端证书", "xpack.securitySolution.certificate.fingerprint.serverCertLabel": "服务器证书", "xpack.securitySolution.chart.allOthersGroupingLabel": "所有其他", @@ -17554,19 +17543,6 @@ "xpack.securitySolution.components.mlPopup.upgradeButtonLabel": "订阅计划", "xpack.securitySolution.components.mlPopup.upgradeDescription": "要访问 SIEM 的异常检测功能,必须将您的许可证更新到白金级、开始 30 天免费试用或在 AWS、GCP 或 Azure 中实施{cloudLink}。然后便可以运行 Machine Learning 作业并查看异常。", "xpack.securitySolution.components.mlPopup.upgradeTitle": "升级到 Elastic 白金级", - "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel": "键入内容进行搜索", - "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder": "键入内容进行搜索", - "xpack.securitySolution.components.settings.jira.searchIssuesLoading": "正在加载……", - "xpack.securitySolution.components.settings.jira.unableToGetFieldsMessage": "无法获取字段", - "xpack.securitySolution.components.settings.jira.unableToGetIssueMessage": "无法获取 ID 为 {id} 的问题", - "xpack.securitySolution.components.settings.jira.unableToGetIssuesMessage": "无法获取问题", - "xpack.securitySolution.components.settings.jira.unableToGetIssueTypesMessage": "无法获取问题类型", - "xpack.securitySolution.components.settings.serviceNow.impactSelectFieldLabel": "影响", - "xpack.securitySolution.components.settings.serviceNow.severitySelectFieldLabel": "严重性", - "xpack.securitySolution.components.settings.servicenow.severitySelectHighOptionLabel": "高", - "xpack.securitySolution.components.settings.servicenow.severitySelectLowOptionLabel": "低", - "xpack.securitySolution.components.settings.servicenow.severitySelectMediumOptionLabel": "中", - "xpack.securitySolution.components.settings.serviceNow.urgencySelectFieldLabel": "紧急性", "xpack.securitySolution.components.stepDefineRule.ruleTypeField.subscriptionsLink": "白金级订阅", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "无法查询异常数据", "xpack.securitySolution.containers.anomalies.stackByJobId": "作业", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts deleted file mode 100644 index df35990da8c0c..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as i18n from './translations'; -import logo from './logo.svg'; - -export const connectorConfiguration = { - id: '.jira', - name: i18n.JIRA_TITLE, - logo, - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'gold', -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 26b37278003c3..ba6a5fa2079dc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -11,7 +11,6 @@ import { ActionTypeModel, ConnectorValidationResult, } from '../../../../types'; -import { connectorConfiguration } from './config'; import logo from './logo.svg'; import { JiraActionConnector, JiraConfig, JiraSecrets, JiraActionParams } from './types'; import * as i18n from './translations'; @@ -63,10 +62,10 @@ const validateConnector = ( export function getActionType(): ActionTypeModel { return { - id: connectorConfiguration.id, + id: '.jira', iconClass: logo, selectMessage: i18n.JIRA_DESC, - actionTypeTitle: connectorConfiguration.name, + actionTypeTitle: i18n.JIRA_TITLE, validateConnector, actionConnectorFields: lazy(() => import('./jira_connectors')), validateParams: (actionParams: JiraActionParams): GenericValidationResult => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts deleted file mode 100644 index 03b434283cd6e..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as i18n from './translations'; -import logo from './logo.svg'; - -export const connectorConfiguration = { - id: '.resilient', - name: i18n.TITLE, - logo, - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx index 3e1eafdfebca8..a8fe5e8ae4b6a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx @@ -11,7 +11,6 @@ import { ActionTypeModel, ConnectorValidationResult, } from '../../../../types'; -import { connectorConfiguration } from './config'; import logo from './logo.svg'; import { ResilientActionConnector, @@ -72,10 +71,10 @@ export function getActionType(): ActionTypeModel< ResilientActionParams > { return { - id: connectorConfiguration.id, + id: '.resilient', iconClass: logo, selectMessage: i18n.DESC, - actionTypeTitle: connectorConfiguration.name, + actionTypeTitle: i18n.TITLE, validateConnector, actionConnectorFields: lazy(() => import('./resilient_connectors')), validateParams: (actionParams: ResilientActionParams): GenericValidationResult => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts deleted file mode 100644 index 3e629261a29ba..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts +++ /dev/null @@ -1,31 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as i18n from './translations'; -import logo from './logo.svg'; - -export const serviceNowITSMConfiguration = { - id: '.servicenow', - name: i18n.SERVICENOW_ITSM_TITLE, - desc: i18n.SERVICENOW_ITSM_DESC, - logo, - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', -}; - -export const serviceNowSIRConfiguration = { - id: '.servicenow-sir', - name: i18n.SERVICENOW_SIR_TITLE, - desc: i18n.SERVICENOW_SIR_DESC, - logo, - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx index 82d7f028a3e3d..b1664656c0d14 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -11,7 +11,6 @@ import { ActionTypeModel, ConnectorValidationResult, } from '../../../../types'; -import { serviceNowITSMConfiguration, serviceNowSIRConfiguration } from './config'; import logo from './logo.svg'; import { ServiceNowActionConnector, @@ -68,10 +67,10 @@ export function getServiceNowITSMActionType(): ActionTypeModel< ServiceNowITSMActionParams > { return { - id: serviceNowITSMConfiguration.id, + id: '.servicenow', iconClass: logo, - selectMessage: serviceNowITSMConfiguration.desc, - actionTypeTitle: serviceNowITSMConfiguration.name, + selectMessage: i18n.SERVICENOW_ITSM_DESC, + actionTypeTitle: i18n.SERVICENOW_ITSM_TITLE, validateConnector, actionConnectorFields: lazy(() => import('./servicenow_connectors')), validateParams: ( @@ -103,10 +102,10 @@ export function getServiceNowSIRActionType(): ActionTypeModel< ServiceNowSIRActionParams > { return { - id: serviceNowSIRConfiguration.id, + id: '.servicenow-sir', iconClass: logo, - selectMessage: serviceNowSIRConfiguration.desc, - actionTypeTitle: serviceNowSIRConfiguration.name, + selectMessage: i18n.SERVICENOW_SIR_DESC, + actionTypeTitle: i18n.SERVICENOW_SIR_TITLE, validateConnector, actionConnectorFields: lazy(() => import('./servicenow_connectors')), validateParams: (actionParams: ServiceNowSIRActionParams): GenericValidationResult => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx index a55811ffa8ffd..bfc32ef67e46f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx @@ -153,49 +153,22 @@ describe('ServiceNowITSMParamsFields renders', () => { }); }); - test('it transforms the urgencies to options correctly', async () => { + test('it transforms the options correctly', async () => { const wrapper = mount(); act(() => { onChoices(useGetChoicesResponse.choices); }); wrapper.update(); - expect(wrapper.find('[data-test-subj="urgencySelect"]').first().prop('options')).toEqual([ - { value: '1', text: '1 - Critical' }, - { value: '2', text: '2 - High' }, - { value: '3', text: '3 - Moderate' }, - { value: '4', text: '4 - Low' }, - ]); - }); - - test('it transforms the severities to options correctly', async () => { - const wrapper = mount(); - act(() => { - onChoices(useGetChoicesResponse.choices); - }); - - wrapper.update(); - expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('options')).toEqual([ - { value: '1', text: '1 - Critical' }, - { value: '2', text: '2 - High' }, - { value: '3', text: '3 - Moderate' }, - { value: '4', text: '4 - Low' }, - ]); - }); - - test('it transforms the impacts to options correctly', async () => { - const wrapper = mount(); - act(() => { - onChoices(useGetChoicesResponse.choices); - }); - - wrapper.update(); - expect(wrapper.find('[data-test-subj="impactSelect"]').first().prop('options')).toEqual([ - { value: '1', text: '1 - Critical' }, - { value: '2', text: '2 - High' }, - { value: '3', text: '3 - Moderate' }, - { value: '4', text: '4 - Low' }, - ]); + const testers = ['severity', 'urgency', 'impact']; + testers.forEach((subj) => + expect(wrapper.find(`[data-test-subj="${subj}Select"]`).first().prop('options')).toEqual([ + { value: '1', text: '1 - Critical' }, + { value: '2', text: '2 - High' }, + { value: '3', text: '3 - Moderate' }, + { value: '4', text: '4 - Low' }, + ]) + ); }); describe('UI updates', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index 1e1ba99633995..288b6e629112d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -17,7 +17,7 @@ export const SERVICENOW_ITSM_DESC = i18n.translate( export const SERVICENOW_SIR_DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.selectMessageText', { - defaultMessage: 'Create an incident in ServiceNow SIR.', + defaultMessage: 'Create an incident in ServiceNow SecOps.', } ); @@ -31,7 +31,7 @@ export const SERVICENOW_ITSM_TITLE = i18n.translate( export const SERVICENOW_SIR_TITLE = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.actionTypeTitle', { - defaultMessage: 'ServiceNow SIR', + defaultMessage: 'ServiceNow SecOps', } ); @@ -172,7 +172,7 @@ export const MALWARE_URL_LABEL = i18n.translate( export const MALWARE_HASH_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareHashTitle', { - defaultMessage: 'Malware hash', + defaultMessage: 'Malware Hash', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts index f16f1dc1bc1cf..01470bdddf4d7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -11,6 +11,9 @@ export * from './index_controls'; export * from './lib'; export * from './types'; -export { serviceNowITSMConfiguration as ServiceNowITSMConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config'; -export { connectorConfiguration as JiraConnectorConfiguration } from '../application/components/builtin_action_types/jira/config'; -export { connectorConfiguration as ResilientConnectorConfiguration } from '../application/components/builtin_action_types/resilient/config'; +export { + getServiceNowITSMActionType, + getServiceNowSIRActionType, +} from '../application/components/builtin_action_types/servicenow'; +export { getJiraActionType } from '../application/components/builtin_action_types/jira'; +export { getResilientActionType } from '../application/components/builtin_action_types/resilient'; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 7c4ee7b9b0de7..878507bcf4afc 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -39,6 +39,9 @@ export function getAllExternalServiceSimulatorPaths(): string[] { getExternalServiceSimulatorPath(service) ); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); + allPaths.push( + `/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident/123` + ); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/sys_choice`); allPaths.push( `/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/sys_dictionary` diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts index fe891dc6c5f34..ef7c57b3b4749 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts @@ -59,7 +59,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); actionsRemover.add('default', connector.id, 'action', 'actions'); - const { body: configure } = await supertest + await supertest .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') .send( @@ -70,6 +70,7 @@ export default ({ getService }: FtrProviderContext): void => { }) ) .expect(200); + const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -79,25 +80,34 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: null, impact: null, severity: null }, + fields: { urgency: '2', impact: '2', severity: '2' }, }).connector, }) .expect(200); const { body } = await supertest - .post(`${CASES_URL}/${postedCase.id}/_push`) + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) .set('kbn-xsrf', 'true') - .send({ - connector_id: configure.connector.id, - connector_name: configure.connector.name, - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', - }) + .send({}) .expect(200); - expect(body.connector.id).to.eql(configure.connector.id); - expect(body.external_service.pushed_by).to.eql(defaultUser); + // eslint-disable-next-line @typescript-eslint/naming-convention + const { pushed_at, external_url, ...rest } = body.external_service; + + expect(rest).to.eql({ + pushed_by: defaultUser, + connector_id: connector.id, + connector_name: connector.name, + external_id: '123', + external_title: 'INC01', + }); + + // external_url is of the form http://elastic:changeme@localhost:5620 which is different between various environments like Jekins + expect( + external_url.includes( + 'api/_actions-FTS-external-service-simulators/servicenow/nav_to.do?uri=incident.do?sys_id=123' + ) + ).to.equal(true); }); it('pushes a comment appropriately', async () => { @@ -112,7 +122,7 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add('default', connector.id, 'action', 'actions'); - const { body: configure } = await supertest + await supertest .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') .send( @@ -133,79 +143,134 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: null, impact: null, severity: null }, + fields: { urgency: '2', impact: '2', severity: '2' }, }).connector, }) .expect(200); await supertest - .post(`${CASES_URL}/${postedCase.id}/_push`) + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body } = await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(200); + + expect(body.comments[0].pushed_by).to.eql(defaultUser); + }); + + it('should pushes a case and closes when closure_type: close-by-pushing', async () => { + const { body: connector } = await supertest + .post('/api/actions/action') .set('kbn-xsrf', 'true') .send({ - connector_id: configure.connector.id, - connector_name: configure.connector.name, - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, }) .expect(200); + actionsRemover.add('default', connector.id, 'action', 'actions'); await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) + .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') - .send(postCommentUserReq) + .send({ + ...getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + }), + closure_type: 'close-by-pushing', + }) .expect(200); - const { body } = await supertest - .post(`${CASES_URL}/${postedCase.id}/_push`) + const { body: postedCase } = await supertest + .post(CASES_URL) .set('kbn-xsrf', 'true') .send({ - connector_id: configure.connector.id, - connector_name: configure.connector.name, - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', + ...postCaseReq, + connector: getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + fields: { urgency: '2', impact: '2', severity: '2' }, + }).connector, }) .expect(200); - expect(body.comments[0].pushed_by).to.eql(defaultUser); + const { body } = await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(200); + + expect(body.status).to.eql('closed'); }); it('unhappy path - 404s when case does not exist', async () => { await supertest - .post(`${CASES_URL}/fake-id/_push`) + .post(`${CASES_URL}/fake-id/connector/fake-connector/_push`) .set('kbn-xsrf', 'true') - .send({ - connector_id: 'connector_id', - connector_name: 'connector_name', - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', - }) + .send({}) .expect(404); }); - it('unhappy path - 400s when bad data supplied', async () => { - await supertest - .post(`${CASES_URL}/fake-id/_push`) + it('unhappy path - 404s when connector does not exist', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) .set('kbn-xsrf', 'true') .send({ - badKey: 'connector_id', + ...postCaseReq, + connector: getConfiguration().connector, }) - .expect(400); + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/fake-connector/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(404); }); it('unhappy path = 409s when case is closed', async () => { - const { body: configure } = await supertest + const { body: connector } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send({ + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }) + .expect(200); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + await supertest .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') - .send(getConfiguration()) + .send( + getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + }) + ) .expect(200); const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq) + .send({ + ...postCaseReq, + connector: getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + fields: { urgency: '2', impact: '2', severity: '2' }, + }).connector, + }) .expect(200); await supertest @@ -223,15 +288,9 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); await supertest - .post(`${CASES_URL}/${postedCase.id}/_push`) + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) .set('kbn-xsrf', 'true') - .send({ - connector_id: configure.connector.id, - connector_name: configure.connector.name, - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', - }) + .send({}) .expect(409); }); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts index d0b6ae53cbcd0..d83d87da1e7af 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts @@ -359,21 +359,15 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: null, impact: null, severity: null }, + fields: { urgency: '2', impact: '2', severity: '2' }, }).connector, }) .expect(200); await supertest - .post(`${CASES_URL}/${postedCase.id}/_push`) + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) .set('kbn-xsrf', 'true') - .send({ - connector_id: configure.connector.id, - connector_name: configure.connector.name, - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', - }) + .send({}) .expect(200); const { body } = await supertest diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 7115576ccccbd..27a49c3f05869 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -14,8 +14,8 @@ import { } from '../../../../plugins/case/common/api'; export const getConfiguration = ({ - id = 'connector-1', - name = 'Connector 1', + id = 'none', + name = 'none', type = ConnectorTypes.none, fields = null, }: Partial = {}): CasesConfigureRequest => { From 19543d8d3c28108270e65d58df97f2dd47b6c986 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 9 Feb 2021 13:51:12 +0300 Subject: [PATCH 045/100] [Vega] user should be able to set a specific tilemap service using the mapStyle property (#88440) * [Vega] user should be able to set a specific tilemap service using the mapStyle property * Update vega-reference.asciidoc * fix PR comments * rename mapStyle -> emsTileServiceId Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/dashboard/vega-reference.asciidoc | 9 +++-- .../public/data_model/vega_parser.test.js | 33 ++++++++----------- .../public/data_model/vega_parser.ts | 19 +++-------- .../public/vega_view/vega_map_view/view.ts | 18 +++++----- .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 6 files changed, 35 insertions(+), 48 deletions(-) diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 2c961dca44474..88fd870fefa74 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -251,9 +251,14 @@ experimental[] To enable *Maps*, the graph must specify `type=map` in the host c "longitude": -74, // default 0 "zoom": 7, // default 2 - // defaults to "default". Use false to disable base layer. + // Defaults to 'true', disables the base map layer. "mapStyle": false, + // When 'mapStyle' is 'undefined' or 'true', sets the EMS-layer for the map. + // May either be: "road_map", "road_map_desaturated", "dark_map". + // If 'emsTileServiceId' is 'undefined', it falls back to the auto-switch-dark-light behavior. + "emsTileServiceId": "road_map", + // default 0 "minZoom": 5, @@ -261,7 +266,7 @@ experimental[] To enable *Maps*, the graph must specify `type=map` in the host c // or 25 when base is disabled "maxZoom": 13, - // defaults to true, shows +/- buttons to zoom in/out + // Defaults to 'true', shows +/- buttons to zoom in/out "zoomControl": false, // Defaults to 'false', disables mouse wheel zoom. If set to diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index 1948792d55a83..f33c2bfc27630 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -279,7 +279,7 @@ describe('VegaParser._parseMapConfig', () => { delayRepaint: true, latitude: 0, longitude: 0, - mapStyle: 'default', + mapStyle: true, zoomControl: true, scrollWheelZoom: false, }, @@ -288,52 +288,47 @@ describe('VegaParser._parseMapConfig', () => { ); test( - 'filled', + 'emsTileServiceId', check( { - delayRepaint: true, - latitude: 0, - longitude: 0, - mapStyle: 'default', - zoomControl: true, - scrollWheelZoom: false, - maxBounds: [1, 2, 3, 4], + mapStyle: true, + emsTileServiceId: 'dark_map', }, { delayRepaint: true, latitude: 0, longitude: 0, - mapStyle: 'default', + mapStyle: true, + emsTileServiceId: 'dark_map', zoomControl: true, scrollWheelZoom: false, - maxBounds: [1, 2, 3, 4], }, 0 ) ); test( - 'warnings', + 'filled', check( { delayRepaint: true, latitude: 0, longitude: 0, - zoom: 'abc', // ignored - mapStyle: 'abc', - zoomControl: 'abc', - scrollWheelZoom: 'abc', - maxBounds: [2, 3, 4], + mapStyle: true, + zoomControl: true, + scrollWheelZoom: false, + maxBounds: [1, 2, 3, 4], }, { delayRepaint: true, latitude: 0, longitude: 0, - mapStyle: 'default', + mapStyle: true, zoomControl: true, scrollWheelZoom: false, + maxBounds: [1, 2, 3, 4], }, - 5 + 0 ) ); }); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index e97418581a42f..d3647b35a5b94 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -465,21 +465,10 @@ The URL is an identifier only. Kibana and your browser will never access this UR validate(`minZoom`, true); validate(`maxZoom`, true); - // `false` is a valid value - res.mapStyle = this._config?.mapStyle === undefined ? `default` : this._config.mapStyle; - if (res.mapStyle !== `default` && res.mapStyle !== false) { - this._onWarning( - i18n.translate('visTypeVega.vegaParser.mapStyleValueTypeWarningMessage', { - defaultMessage: - '{mapStyleConfigName} may either be {mapStyleConfigFirstAllowedValue} or {mapStyleConfigSecondAllowedValue}', - values: { - mapStyleConfigName: 'config.kibana.mapStyle', - mapStyleConfigFirstAllowedValue: 'false', - mapStyleConfigSecondAllowedValue: '"default"', - }, - }) - ); - res.mapStyle = `default`; + this._parseBool('mapStyle', res, true); + + if (res.mapStyle) { + res.emsTileServiceId = this._config?.emsTileServiceId; } this._parseBool('zoomControl', res, true); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index 4c155d6b5ea88..c2112659a50ae 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -52,12 +52,14 @@ async function updateVegaView(mapBoxInstance: Map, vegaView: View) { export class VegaMapView extends VegaBaseView { private mapServiceSettings: MapServiceSettings = getMapServiceSettings(); - private mapStyle = this.getMapStyle(); + private emsTileLayer = this.getEmsTileLayer(); - private getMapStyle() { - const { mapStyle } = this._parser.mapConfig; + private getEmsTileLayer() { + const { mapStyle, emsTileServiceId } = this._parser.mapConfig; - return mapStyle === 'default' ? this.mapServiceSettings.defaultTmsLayer() : mapStyle; + if (mapStyle) { + return emsTileServiceId ?? this.mapServiceSettings.defaultTmsLayer(); + } } private get shouldShowZoomControl() { @@ -83,14 +85,14 @@ export class VegaMapView extends VegaBaseView { maxZoom: defaultMapConfig.maxZoom, }; - if (this.mapStyle && this.mapStyle !== userConfiguredLayerId) { - const tmsService = await this.mapServiceSettings.getTmsService(this.mapStyle); + if (this.emsTileLayer && this.emsTileLayer !== userConfiguredLayerId) { + const tmsService = await this.mapServiceSettings.getTmsService(this.emsTileLayer); if (!tmsService) { this.onWarn( i18n.translate('visTypeVega.mapView.mapStyleNotFoundWarningMessage', { defaultMessage: '{mapStyleParam} was not found', - values: { mapStyleParam: `"mapStyle":${this.mapStyle}` }, + values: { mapStyleParam: `"emsTileServiceId":${this.emsTileLayer}` }, }) ); return; @@ -138,7 +140,7 @@ export class VegaMapView extends VegaBaseView { } private initLayers(mapBoxInstance: Map, vegaView: View) { - const shouldShowUserConfiguredLayer = this.mapStyle === userConfiguredLayerId; + const shouldShowUserConfiguredLayer = this.emsTileLayer === userConfiguredLayerId; if (shouldShowUserConfiguredLayer) { const { url, options } = this.mapServiceSettings.config.tilemap; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5e61f58e7afac..b8a67d9c3388e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4553,7 +4553,6 @@ "visTypeVega.inspector.vegaAdapter.value": "値", "visTypeVega.inspector.vegaDebugLabel": "Vegaデバッグ", "visTypeVega.mapView.experimentalMapLayerInfo": "マップレイヤーはまだ実験段階であり、オフィシャルGA機能のサポートSLAが適用されません。フィードバックがある場合は、{githubLink}で問題を報告してください。", - "visTypeVega.mapView.mapStyleNotFoundWarningMessage": "{mapStyleParam} が見つかりませんでした", "visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage": "{minZoomPropertyName} と {maxZoomPropertyName} が交換されました", "visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage": "{name} を {max} にリセットしています", "visTypeVega.mapView.resettingPropertyToMinValueWarningMessage": "{name} を {min} にリセットしています", @@ -4575,7 +4574,6 @@ "visTypeVega.vegaParser.inputSpecDoesNotSpecifySchemaErrorMessage": "仕様に基づき、{schemaParam}フィールドには、\nVega({vegaSchemaUrl}を参照)または\nVega-Lite({vegaLiteSchemaUrl}を参照)の有効なURLを入力する必要があります。\nURLは識別子にすぎません。Kibanaやご使用のブラウザーがこのURLにアクセスすることはありません。", "visTypeVega.vegaParser.invalidVegaSpecErrorMessage": "無効な Vega 仕様", "visTypeVega.vegaParser.kibanaConfigValueTypeErrorMessage": "{configName} が含まれている場合、オブジェクトでなければなりません", - "visTypeVega.vegaParser.mapStyleValueTypeWarningMessage": "{mapStyleConfigName} は {mapStyleConfigFirstAllowedValue} か {mapStyleConfigSecondAllowedValue} のどちらかです", "visTypeVega.vegaParser.maxBoundsValueTypeWarningMessage": "{maxBoundsConfigName} は 4 つの数字の配列でなければなりません", "visTypeVega.vegaParser.notSupportedUrlTypeErrorMessage": "{urlObject} はサポートされていません", "visTypeVega.vegaParser.notValidLibraryVersionForInputSpecWarningMessage": "インプット仕様に {schemaLibrary} {schemaVersion} が使用されていますが、現在のバージョンの {schemaLibrary} は {libraryVersion} です。’", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 14e26395ad3ce..229265fe62252 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4558,7 +4558,6 @@ "visTypeVega.inspector.vegaAdapter.value": "值", "visTypeVega.inspector.vegaDebugLabel": "Vega 调试", "visTypeVega.mapView.experimentalMapLayerInfo": "地图图层处于试验状态,不受正式发行版功能的支持 SLA 的约束。如欲提供反馈,请在 {githubLink} 中创建问题。", - "visTypeVega.mapView.mapStyleNotFoundWarningMessage": "找不到 {mapStyleParam}", "visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage": "已互换 {minZoomPropertyName} 和 {maxZoomPropertyName}", "visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage": "将 {name} 重置为 {max}", "visTypeVega.mapView.resettingPropertyToMinValueWarningMessage": "将 {name} 重置为 {min}", @@ -4580,7 +4579,6 @@ "visTypeVega.vegaParser.inputSpecDoesNotSpecifySchemaErrorMessage": "您的规范要求 {schemaParam} 字段包含\nVega(请参见 {vegaSchemaUrl})或\nVega-Lite(请参见 {vegaLiteSchemaUrl})的有效 URL。\n该 URL 仅限标识符。Kibana 和您的浏览器将不访问此 URL。", "visTypeVega.vegaParser.invalidVegaSpecErrorMessage": "Vega 规范无效", "visTypeVega.vegaParser.kibanaConfigValueTypeErrorMessage": "如果存在,{configName} 必须为对象", - "visTypeVega.vegaParser.mapStyleValueTypeWarningMessage": "{mapStyleConfigName} 可能为 {mapStyleConfigFirstAllowedValue} 或 {mapStyleConfigSecondAllowedValue}", "visTypeVega.vegaParser.maxBoundsValueTypeWarningMessage": "{maxBoundsConfigName} 必须为具有四个数字的数组", "visTypeVega.vegaParser.notSupportedUrlTypeErrorMessage": "不支持 {urlObject}", "visTypeVega.vegaParser.notValidLibraryVersionForInputSpecWarningMessage": "输入规范使用 {schemaLibrary} {schemaVersion},但 {schemaLibrary} 的当前版本为 {libraryVersion}。", From 4685e8c06ca4d4ad4369a4a41ee1488c35dc2dde Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 9 Feb 2021 11:53:53 +0100 Subject: [PATCH 046/100] before/beforeEach clean up (#90663) --- .../indicator_match_rule.spec.ts | 32 ++++++++++++------- .../cypress/urls/navigation.ts | 1 + 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 98e6dad350ea7..a69f808001800 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -96,7 +96,7 @@ import { import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; -import { DETECTIONS_URL } from '../../urls/navigation'; +import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; describe('Detection rules, Indicator Match', () => { const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); @@ -106,25 +106,22 @@ describe('Detection rules, Indicator Match', () => { const expectedNumberOfRules = 1; const expectedNumberOfAlerts = 1; - beforeEach(() => { + before(() => { cleanKibana(); esArchiverLoad('threat_indicator'); esArchiverLoad('threat_data'); - loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); - waitForAlertsIndexToBeCreated(); - goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); - goToCreateNewRule(); - selectIndicatorMatchType(); }); - - afterEach(() => { + after(() => { esArchiverUnload('threat_indicator'); esArchiverUnload('threat_data'); }); describe('Creating new indicator match rules', () => { + beforeEach(() => { + loginAndWaitForPageWithoutDateRange(RULE_CREATION); + selectIndicatorMatchType(); + }); + describe('Index patterns', () => { it('Contains a predefined index pattern', () => { getIndicatorIndex().should('have.text', indexPatterns.join('')); @@ -355,6 +352,19 @@ describe('Detection rules, Indicator Match', () => { getIndicatorMappingComboField(2).should('not.exist'); }); }); + }); + + describe('Generating signals', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectIndicatorMatchType(); + }); it('Creates and activates a new Indicator Match rule', () => { fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule); diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts index f3881ab624f7b..2beed9e8ec0b7 100644 --- a/x-pack/plugins/security_solution/cypress/urls/navigation.ts +++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts @@ -24,5 +24,6 @@ export const KIBANA_HOME = '/app/home#/'; export const ADMINISTRATION_URL = '/app/security/administration'; export const NETWORK_URL = '/app/security/network'; export const OVERVIEW_URL = '/app/security/overview'; +export const RULE_CREATION = 'app/security/detections/rules/create'; export const TIMELINES_URL = '/app/security/timelines'; export const TIMELINE_TEMPLATES_URL = '/app/security/timelines/template'; From af75079a31e98c8a6c0141393617b39eeb39d9bf Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 9 Feb 2021 05:54:47 -0500 Subject: [PATCH 047/100] [Fleet] Use TS project references (#87574) ## Summary * Added references to all dependencies https://github.com/elastic/kibana/blob/6bc6f3459a120eddfae70ad2fc7e4669e3a996b0/x-pack/plugins/fleet/tsconfig.json#L17-L38 * `node scripts/check_ts_projects` is successful * `node scripts/build_ts_refs` is successful
node --max-old-space-size=4096 ./node_modules/.bin/tsc -p tsconfig.json --extendedDiagnostics --noEmit ``` Files: 1436 Lines: 267372 Nodes: 1016769 Identifiers: 361835 Symbols: 250405 Types: 31105 Instantiations: 57570 Memory used: 347817K Assignability cache size: 5597 Identity cache size: 3073 Subtype cache size: 2140 Strict subtype cache size: 1012 I/O Read time: 0.49s Parse time: 3.84s ResolveModule time: 1.59s ResolveTypeReference time: 0.16s Program time: 7.46s Bind time: 1.87s Check time: 4.02s printTime time: 0.00s Emit time: 0.00s Total time: 13.35s ```
--- x-pack/plugins/fleet/tsconfig.json | 39 ++++++++++++++++++++++++++++++ x-pack/test/tsconfig.json | 5 ++++ x-pack/tsconfig.json | 1 + x-pack/tsconfig.refs.json | 1 + 4 files changed, 46 insertions(+) create mode 100644 x-pack/plugins/fleet/tsconfig.json diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json new file mode 100644 index 0000000000000..3a37b14410424 --- /dev/null +++ b/x-pack/plugins/fleet/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + // add all the folders containg files to be compiled + "common/**/*", + "public/**/*", + "server/**/*", + "scripts/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + // add references to other TypeScript projects the plugin depends on + + // requiredPlugins from ./kibana.json + { "path": "../licensing/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../encrypted_saved_objects/tsconfig.json" }, + + // optionalPlugins from ./kibana.json + { "path": "../security/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../cloud/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + + // requiredBundles from ./kibana.json + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../infra/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 2981346e80e1d..7ba5c00a71b37 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -45,6 +45,11 @@ { "path": "../plugins/code/tsconfig.json" }, { "path": "../plugins/console_extensions/tsconfig.json" }, { "path": "../plugins/dashboard_mode/tsconfig.json" }, + { "path": "../plugins/enterprise_search/tsconfig.json" }, + { "path": "../plugins/fleet/tsconfig.json" }, + { "path": "../plugins/global_search/tsconfig.json" }, + { "path": "../plugins/global_search_providers/tsconfig.json" }, + { "path": "../plugins/features/tsconfig.json" }, { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/embeddable_enhanced/tsconfig.json" }, { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 740bac3f1b0de..3afbb027e7fde 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -14,6 +14,7 @@ "plugins/discover_enhanced/**/*", "plugins/dashboard_mode/**/*", "plugins/dashboard_enhanced/**/*", + "plugins/fleet/**/*", "plugins/global_search/**/*", "plugins/global_search_providers/**/*", "plugins/graph/**/*", diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index 7a2eebc78b69b..54cee9b124237 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -18,6 +18,7 @@ { "path": "./plugins/event_log/tsconfig.json" }, { "path": "./plugins/features/tsconfig.json" }, { "path": "./plugins/file_upload/tsconfig.json" }, + { "path": "./plugins/fleet/tsconfig.json" }, { "path": "./plugins/global_search_bar/tsconfig.json" }, { "path": "./plugins/global_search_providers/tsconfig.json" }, { "path": "./plugins/global_search/tsconfig.json" }, From 523110d4bda9136780e7299d737f83c16d0af30f Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Tue, 9 Feb 2021 12:45:19 +0100 Subject: [PATCH 048/100] Functional tests - Add esSupertest support for SSL (#90425) This PR allows the functional test service esSupertest to work correctly in environments that have ES SSL enabled in the Kibana server configuration. --- test/api_integration/services/supertest.ts | 12 ++++++++++-- x-pack/test/functional_with_es_ssl/config.ts | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/test/api_integration/services/supertest.ts b/test/api_integration/services/supertest.ts index 1257a934da8be..a0268b78cb151 100644 --- a/test/api_integration/services/supertest.ts +++ b/test/api_integration/services/supertest.ts @@ -19,6 +19,14 @@ export function KibanaSupertestProvider({ getService }: FtrProviderContext) { export function ElasticsearchSupertestProvider({ getService }: FtrProviderContext) { const config = getService('config'); - const elasticSearchServerUrl = formatUrl(config.get('servers.elasticsearch')); - return supertestAsPromised(elasticSearchServerUrl); + const esServerConfig = config.get('servers.elasticsearch'); + const elasticSearchServerUrl = formatUrl(esServerConfig); + + let agentOptions = {}; + if ('certificateAuthorities' in esServerConfig) { + agentOptions = { ca: esServerConfig!.certificateAuthorities }; + } + + // @ts-ignore - supertestAsPromised doesn't like the agentOptions, but still passes it correctly to supertest + return supertestAsPromised.agent(elasticSearchServerUrl, agentOptions); } diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 5be8eee3155b9..a7259f2410d6b 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -5,6 +5,7 @@ * 2.0. */ +import Fs from 'fs'; import { resolve, join } from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; @@ -33,6 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { elasticsearch: { ...xpackFunctionalConfig.get('servers.elasticsearch'), protocol: 'https', + certificateAuthorities: [Fs.readFileSync(CA_CERT_PATH)], }, }; From 6bd7f7df20885d1f75ce6fa618b9fdd08e4104ba Mon Sep 17 00:00:00 2001 From: ymao1 Date: Tue, 9 Feb 2021 07:41:15 -0500 Subject: [PATCH 049/100] [Alerting] Unit tests for Index Threshold Alert Components (#90285) * Added unit test for index threshold expression * Fixing warnings in index select popover test * Added test for index select popover * Added initial test for threshold visualization * Unit tests * License * Visualization unit tests * Fixing types check Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/index_select_popover.test.tsx | 154 +++++++++---- .../alert_types/es_query/expression.test.tsx | 3 - .../alert_types/threshold/expression.test.tsx | 218 ++++++++++++++++++ .../alert_types/threshold/expression.tsx | 18 +- .../threshold/visualization.test.tsx | 187 +++++++++++++++ .../alert_types/threshold/visualization.tsx | 3 + .../common/expression_items/for_the_last.tsx | 1 + .../common/expression_items/group_by_over.tsx | 1 + 8 files changed, 528 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.test.tsx diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx index 64c085a823478..3b7baac9b80e6 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx @@ -9,54 +9,58 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { IndexSelectPopover } from './index_select_popover'; +import { EuiComboBox } from '@elastic/eui'; -jest.mock('../../../../triggers_actions_ui/public', () => ({ - getIndexPatterns: () => { - return ['index1', 'index2']; - }, - firstFieldOption: () => { - return { text: 'Select a field', value: '' }; - }, - getTimeFieldOptions: () => { - return [ - { - text: '@timestamp', - value: '@timestamp', - }, - ]; - }, - getFields: () => { - return Promise.resolve([ - { - name: '@timestamp', - type: 'date', - }, - { - name: 'field', - type: 'text', - }, - ]); - }, - getIndexOptions: () => { - return Promise.resolve([ - { - label: 'indexOption', - options: [ - { - label: 'index1', - value: 'index1', - }, - { - label: 'index2', - value: 'index2', - }, - ], - }, - ]); - }, -})); +jest.mock('../../../../triggers_actions_ui/public', () => { + const original = jest.requireActual('../../../../triggers_actions_ui/public'); + return { + ...original, + getIndexPatterns: () => { + return ['index1', 'index2']; + }, + getTimeFieldOptions: () => { + return [ + { + text: '@timestamp', + value: '@timestamp', + }, + ]; + }, + getFields: () => { + return Promise.resolve([ + { + name: '@timestamp', + type: 'date', + }, + { + name: 'field', + type: 'text', + }, + ]); + }, + getIndexOptions: () => { + return Promise.resolve([ + { + label: 'indexOption', + options: [ + { + label: 'index1', + value: 'index1', + }, + { + label: 'index2', + value: 'index2', + }, + ], + }, + ]); + }, + }; +}); describe('IndexSelectPopover', () => { + const onIndexChange = jest.fn(); + const onTimeFieldChange = jest.fn(); const props = { index: [], esFields: [], @@ -65,8 +69,8 @@ describe('IndexSelectPopover', () => { index: [], timeField: [], }, - onIndexChange: jest.fn(), - onTimeFieldChange: jest.fn(), + onIndexChange, + onTimeFieldChange, }; beforeEach(() => { @@ -106,10 +110,62 @@ describe('IndexSelectPopover', () => { const indexComboBox = wrapper.find('#indexSelectSearchBox'); indexComboBox.first().simulate('click'); - const event = { target: { value: 'indexPattern1' } }; - indexComboBox.find('input').first().simulate('change', event); + + await act(async () => { + const event = { target: { value: 'indexPattern1' } }; + indexComboBox.find('input').first().simulate('change', event); + await nextTick(); + wrapper.update(); + }); const updatedIndexSearchValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); expect(updatedIndexSearchValue.first().props().value).toEqual('indexPattern1'); + + const thresholdComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="thresholdIndexesComboBox"]'); + const thresholdOptions = thresholdComboBox.prop('options'); + expect(thresholdOptions.length > 0).toBeTruthy(); + + await act(async () => { + thresholdComboBox.prop('onChange')!([thresholdOptions[0].options![0]]); + await nextTick(); + wrapper.update(); + }); + expect(onIndexChange).toHaveBeenCalledWith( + [thresholdOptions[0].options![0]].map((opt) => opt.value) + ); + + const timeFieldSelect = wrapper.find('select[data-test-subj="thresholdAlertTimeFieldSelect"]'); + await act(async () => { + timeFieldSelect.simulate('change', { target: { value: '@timestamp' } }); + await nextTick(); + wrapper.update(); + }); + expect(onTimeFieldChange).toHaveBeenCalledWith('@timestamp'); + }); + + test('renders index and timeField if defined', async () => { + const index = 'test-index'; + const timeField = '@timestamp'; + const indexSelectProps = { + ...props, + index: [index], + timeField, + }; + const wrapper = mountWithIntl(); + expect(wrapper.find('button[data-test-subj="selectIndexExpression"]').text()).toEqual( + `index ${index}` + ); + + wrapper.find('[data-test-subj="selectIndexExpression"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect( + wrapper.find('EuiSelect[data-test-subj="thresholdAlertTimeFieldSelect"]').text() + ).toEqual(`Select a field${timeField}`); }); }); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx index 3349de086d982..0a9f94f8efae2 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx @@ -58,9 +58,6 @@ jest.mock('../../../../triggers_actions_ui/public', () => { getIndexPatterns: () => { return ['index1', 'index2']; }, - firstFieldOption: () => { - return { text: 'Select a field', value: '' }; - }, getTimeFieldOptions: () => { return [ { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx new file mode 100644 index 0000000000000..01c2bc18f35e8 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import IndexThresholdAlertTypeExpression, { DEFAULT_VALUES } from './expression'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { IndexThresholdAlertParams } from './types'; +import { validateExpression } from './validation'; +import { + builtInAggregationTypes, + builtInComparators, + getTimeUnitLabel, + TIME_UNITS, +} from '../../../../triggers_actions_ui/public'; + +jest.mock('../../../../triggers_actions_ui/public', () => { + const original = jest.requireActual('../../../../triggers_actions_ui/public'); + return { + ...original, + getIndexPatterns: () => { + return ['index1', 'index2']; + }, + getTimeFieldOptions: () => { + return [ + { + text: '@timestamp', + value: '@timestamp', + }, + ]; + }, + getFields: () => { + return Promise.resolve([ + { + name: '@timestamp', + type: 'date', + }, + { + name: 'field', + type: 'text', + }, + ]); + }, + getIndexOptions: () => { + return Promise.resolve([ + { + label: 'indexOption', + options: [ + { + label: 'index1', + value: 'index1', + }, + { + label: 'index2', + value: 'index2', + }, + ], + }, + ]); + }, + }; +}); + +const dataMock = dataPluginMock.createStartContract(); +const chartsStartMock = chartPluginMock.createStartContract(); + +describe('IndexThresholdAlertTypeExpression', () => { + function getAlertParams(overrides = {}) { + return { + index: 'test-index', + aggType: 'count', + thresholdComparator: '>', + threshold: [0], + timeWindowSize: 15, + timeWindowUnit: 's', + ...overrides, + }; + } + async function setup(alertParams: IndexThresholdAlertParams) { + const { errors } = validateExpression(alertParams); + + const wrapper = mountWithIntl( + {}} + setAlertProperty={() => {}} + errors={errors} + data={dataMock} + defaultActionGroupId="" + actionGroups={[]} + charts={chartsStartMock} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + return wrapper; + } + + test(`should render IndexThresholdAlertTypeExpression with expected components when aggType doesn't require field`, async () => { + const wrapper = await setup(getAlertParams()); + expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="whenExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="groupByExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="aggTypeExpression"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeFalsy(); + }); + + test(`should render IndexThresholdAlertTypeExpression with expected components when aggType does require field`, async () => { + const wrapper = await setup(getAlertParams({ aggType: 'avg' })); + expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="whenExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="groupByExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="aggTypeExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeFalsy(); + }); + + test(`should render IndexThresholdAlertTypeExpression with visualization when there are no expression errors`, async () => { + const wrapper = await setup(getAlertParams({ timeField: '@timestamp' })); + expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeTruthy(); + }); + + test(`should set default alert params when params are undefined`, async () => { + const wrapper = await setup( + getAlertParams({ + aggType: undefined, + thresholdComparator: undefined, + timeWindowSize: undefined, + timeWindowUnit: undefined, + groupBy: undefined, + threshold: undefined, + }) + ); + + expect(wrapper.find('button[data-test-subj="selectIndexExpression"]').text()).toEqual( + 'index test-index' + ); + expect(wrapper.find('button[data-test-subj="whenExpression"]').text()).toEqual( + `when ${builtInAggregationTypes[DEFAULT_VALUES.AGGREGATION_TYPE].text}` + ); + expect(wrapper.find('button[data-test-subj="groupByExpression"]').text()).toEqual( + `over ${DEFAULT_VALUES.GROUP_BY} documents ` + ); + expect(wrapper.find('[data-test-subj="aggTypeExpression"]').exists()).toBeFalsy(); + expect(wrapper.find('button[data-test-subj="thresholdPopover"]').text()).toEqual( + `${builtInComparators[DEFAULT_VALUES.THRESHOLD_COMPARATOR].text} ` + ); + expect(wrapper.find('button[data-test-subj="forLastExpression"]').text()).toEqual( + `for the last ${DEFAULT_VALUES.TIME_WINDOW_SIZE} ${getTimeUnitLabel( + DEFAULT_VALUES.TIME_WINDOW_UNIT as TIME_UNITS, + DEFAULT_VALUES.TIME_WINDOW_SIZE.toString() + )}` + ); + expect( + wrapper.find('EuiEmptyPrompt[data-test-subj="visualizationPlaceholder"]').text() + ).toEqual(`Complete the expression to generate a preview.`); + }); + + test(`should use alert params when params are defined`, async () => { + const aggType = 'avg'; + const thresholdComparator = 'between'; + const timeWindowSize = 987; + const timeWindowUnit = 's'; + const threshold = [3, 1003]; + const groupBy = 'top'; + const termSize = '27'; + const termField = 'host.name'; + const wrapper = await setup( + getAlertParams({ + aggType, + thresholdComparator, + timeWindowSize, + timeWindowUnit, + termSize, + termField, + groupBy, + threshold, + }) + ); + + expect(wrapper.find('button[data-test-subj="whenExpression"]').text()).toEqual( + `when ${builtInAggregationTypes[aggType].text}` + ); + expect(wrapper.find('button[data-test-subj="groupByExpression"]').text()).toEqual( + `grouped over ${groupBy} ${termSize} '${termField}'` + ); + + expect(wrapper.find('button[data-test-subj="thresholdPopover"]').text()).toEqual( + `${builtInComparators[thresholdComparator].text} ${threshold[0]} AND ${threshold[1]}` + ); + expect(wrapper.find('button[data-test-subj="forLastExpression"]').text()).toEqual( + `for the last ${timeWindowSize} ${getTimeUnitLabel( + timeWindowUnit as TIME_UNITS, + timeWindowSize.toString() + )}` + ); + expect( + wrapper.find('EuiEmptyPrompt[data-test-subj="visualizationPlaceholder"]').text() + ).toEqual(`Complete the expression to generate a preview.`); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index aed115a53fa26..380e2793043f8 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -28,7 +28,7 @@ import { IndexThresholdAlertParams } from './types'; import './expression.scss'; import { IndexSelectPopover } from '../components/index_select_popover'; -const DEFAULT_VALUES = { +export const DEFAULT_VALUES = { AGGREGATION_TYPE: 'count', TERM_SIZE: 5, THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN, @@ -100,7 +100,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< alertParams[errorKey as keyof IndexThresholdAlertParams] !== undefined ); - const canShowVizualization = !!Object.keys(errors).find( + const cannotShowVisualization = !!Object.keys(errors).find( (errorKey) => expressionFieldsWithValidation.includes(errorKey) && errors[errorKey].length >= 1 ); @@ -158,6 +158,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< setAlertParams('aggType', selectedAggType) @@ -196,6 +198,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< {aggType && builtInAggregationTypes[aggType].fieldRequired ? ( @@ -258,9 +264,10 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< />
- {canShowVizualization ? ( + {cannotShowVisualization ? ( @@ -275,6 +282,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< ) : ( ({ + getThresholdAlertVisualizationData: jest.fn(() => + Promise.resolve({ + results: [ + { group: 'a', metrics: [['b', 2]] }, + { group: 'a', metrics: [['b', 10]] }, + ], + }) + ), +})); + +const { getThresholdAlertVisualizationData } = jest.requireMock('./index_threshold_api'); + +const dataMock = dataPluginMock.createStartContract(); +const chartsStartMock = chartPluginMock.createStartContract(); +dataMock.fieldFormats = ({ + getDefaultInstance: jest.fn(() => ({ + convert: jest.fn((s: unknown) => JSON.stringify(s)), + })), +} as unknown) as DataPublicPluginStart['fieldFormats']; + +describe('ThresholdVisualization', () => { + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + uiSettings: uiSettingsServiceMock.createSetupContract(), + }, + }); + }); + + const alertParams = { + index: 'test-index', + aggType: 'count', + thresholdComparator: '>', + threshold: [0], + timeWindowSize: 15, + timeWindowUnit: 's', + }; + + async function setup() { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + return wrapper; + } + + test('periodically requests visualization data', async () => { + const refreshRate = 10; + jest.useFakeTimers(); + + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(getThresholdAlertVisualizationData).toHaveBeenCalledTimes(1); + + for (let i = 1; i <= 5; i++) { + await act(async () => { + jest.advanceTimersByTime(refreshRate); + await nextTick(); + wrapper.update(); + }); + expect(getThresholdAlertVisualizationData).toHaveBeenCalledTimes(i + 1); + } + }); + + test('renders loading message on initial load', async () => { + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="firstLoad"]').exists()).toBeTruthy(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="firstLoad"]').exists()).toBeFalsy(); + expect(getThresholdAlertVisualizationData).toHaveBeenCalled(); + }); + + test('renders chart when visualization results are available', async () => { + const wrapper = await setup(); + + expect(wrapper.find('[data-test-subj="alertVisualizationChart"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="noDataCallout"]').exists()).toBeFalsy(); + expect(wrapper.find(Chart)).toHaveLength(1); + expect(wrapper.find(LineSeries)).toHaveLength(1); + expect(wrapper.find(LineAnnotation)).toHaveLength(1); + }); + + test('renders multiple line series chart when visualization results contain multiple groups', async () => { + getThresholdAlertVisualizationData.mockImplementation(() => + Promise.resolve({ + results: [ + { group: 'a', metrics: [['b', 2]] }, + { group: 'a', metrics: [['b', 10]] }, + { group: 'c', metrics: [['d', 1]] }, + ], + }) + ); + + const wrapper = await setup(); + + expect(wrapper.find('[data-test-subj="alertVisualizationChart"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="noDataCallout"]').exists()).toBeFalsy(); + expect(wrapper.find(Chart)).toHaveLength(1); + expect(wrapper.find(LineSeries)).toHaveLength(2); + expect(wrapper.find(LineAnnotation)).toHaveLength(1); + }); + + test('renders error message when getting visualization fails', async () => { + const errorMessage = 'oh no'; + getThresholdAlertVisualizationData.mockImplementation(() => Promise.reject(errorMessage)); + const wrapper = await setup(); + + expect(wrapper.find('[data-test-subj="errorCallout"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="errorCallout"]').first().text()).toBe( + `Cannot load alert visualization${errorMessage}` + ); + }); + + test('renders no data message when visualization results are empty', async () => { + getThresholdAlertVisualizationData.mockImplementation(() => Promise.resolve({ results: [] })); + const wrapper = await setup(); + + expect(wrapper.find('[data-test-subj="alertVisualizationChart"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="noDataCallout"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="noDataCallout"]').first().text()).toBe( + `No data matches this queryCheck that your time range and filters are correct.` + ); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx index 7401d0e26be68..40736f7350b1b 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx @@ -202,6 +202,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ if (loadingState === LoadingStateType.FirstLoad) { return ( } body={ @@ -220,6 +221,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ = ({ ) : ( Date: Tue, 9 Feb 2021 13:27:49 +0000 Subject: [PATCH 050/100] [Security Solution] Update open timeline filters and add unit tests (#89852) * update filter and add unit tests * styling * fix i18n error * fix unit test * fix lint error Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../open_timeline_modal_body.tsx | 12 +- .../open_timeline/title_row/index.tsx | 2 +- .../components/open_timeline/translations.ts | 8 +- .../components/open_timeline/types.ts | 2 - .../open_timeline/use_timeline_types.test.tsx | 193 ++++++++++++++++++ .../open_timeline/use_timeline_types.tsx | 71 +++---- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 8 files changed, 228 insertions(+), 62 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx index 13b9c9ef4f519..1616c5e84247f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiModalBody, EuiModalHeader } from '@elastic/eui'; +import { EuiModalBody, EuiModalHeader, EuiSpacer } from '@elastic/eui'; import React, { Fragment, memo, useMemo } from 'react'; import styled from 'styled-components'; @@ -62,11 +62,10 @@ export const OpenTimelineModalBody = memo( const SearchRowContent = useMemo( () => ( - {!!timelineFilter && timelineFilter} {!!templateTimelineFilter && templateTimelineFilter} ), - [timelineFilter, templateTimelineFilter] + [templateTimelineFilter] ); return ( @@ -84,9 +83,14 @@ export const OpenTimelineModalBody = memo( <> + {!!timelineFilter && ( + <> + {timelineFilter} + + + )} & { */ export const TitleRow = React.memo( ({ children, onAddTimelinesToFavorites, selectedTimelinesCount, title }) => ( - + {onAddTimelinesToFavorites && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts index 84907c74cdace..ae743ad30eef1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts @@ -146,7 +146,7 @@ export const OPEN_TIMELINE = i18n.translate( export const OPEN_TIMELINE_TITLE = i18n.translate( 'xpack.securitySolution.open.timeline.openTimelineTitle', { - defaultMessage: 'Open Timeline', + defaultMessage: 'Open', } ); @@ -274,12 +274,6 @@ export const SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES = (totalTimelineTemplates: } ); -export const FILTER_TIMELINES = (timelineType: string) => - i18n.translate('xpack.securitySolution.open.timeline.filterByTimelineTypesTitle', { - values: { timelineType }, - defaultMessage: 'Only {timelineType}', - }); - export const TAB_TIMELINES = i18n.translate( 'xpack.securitySolution.timelines.components.tabs.timelinesTitle', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index ddf567edafe13..ad62bda4c9783 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -221,13 +221,11 @@ export enum TimelineTabsStyle { } export interface TimelineTab { - count: number | undefined; disabled: boolean; href: string; id: TimelineTypeLiteral; name: string; onClick: (ev: { preventDefault: () => void }) => void; - withNext: boolean; } export interface TemplateTimelineFilter { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx new file mode 100644 index 0000000000000..1d39dd169ffaa --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { + useTimelineTypes, + UseTimelineTypesArgs, + UseTimelineTypesResult, +} from './use_timeline_types'; + +jest.mock('react-router-dom', () => { + return { + useParams: jest.fn().mockReturnValue('default'), + useHistory: jest.fn().mockReturnValue([]), + }; +}); + +jest.mock('../../../common/components/link_to', () => { + return { + getTimelineTabsUrl: jest.fn(), + useFormatUrl: jest.fn().mockReturnValue({ + formatUrl: jest.fn(), + search: '', + }), + }; +}); + +describe('useTimelineTypes', () => { + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + expect(result.current).toEqual({ + timelineType: 'default', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + + describe('timelineTabs', () => { + it('render timelineTabs', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(result.current.timelineTabs); + expect( + container.querySelector('[data-test-subj="timeline-tab-default"]') + ).toHaveTextContent('Timelines'); + expect( + container.querySelector('[data-test-subj="timeline-tab-template"]') + ).toHaveTextContent('Templates'); + }); + }); + + it('set timelineTypes correctly', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(result.current.timelineTabs); + + fireEvent( + container.querySelector('[data-test-subj="timeline-tab-template"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'template', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + + it('stays in the same tab if clicking again on current tab', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(result.current.timelineTabs); + + fireEvent( + container.querySelector('[data-test-subj="timeline-tab-default"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'default', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + }); + + describe('timelineFilters', () => { + it('render timelineFilters', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(<>{result.current.timelineFilters}); + expect( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]') + ).toHaveTextContent('Timelines'); + expect( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]') + ).toHaveTextContent('Templates'); + }); + }); + + it('set timelineTypes correctly', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(<>{result.current.timelineFilters}); + + fireEvent( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'template', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + + it('stays in the same tab if clicking again on current tab', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(<>{result.current.timelineFilters}); + + fireEvent( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'default', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 728d8b6eeb488..a66fe43d305f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -7,7 +7,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui'; +import { EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; import { noop } from 'lodash/fp'; import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline'; @@ -24,7 +24,7 @@ export interface UseTimelineTypesArgs { export interface UseTimelineTypesResult { timelineType: TimelineTypeLiteralWithNull; timelineTabs: JSX.Element; - timelineFilters: JSX.Element[]; + timelineFilters: JSX.Element; } export const useTimelineTypes = ({ @@ -59,51 +59,28 @@ export const useTimelineTypes = ({ (timelineTabsStyle: TimelineTabsStyle) => [ { id: TimelineType.default, - name: - timelineTabsStyle === TimelineTabsStyle.filter - ? i18n.FILTER_TIMELINES(i18n.TAB_TIMELINES) - : i18n.TAB_TIMELINES, + name: i18n.TAB_TIMELINES, href: formatUrl(getTimelineTabsUrl(TimelineType.default, urlSearch)), disabled: false, - withNext: true, - count: - timelineTabsStyle === TimelineTabsStyle.filter - ? defaultTimelineCount ?? undefined - : undefined, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTimeline : noop, }, { id: TimelineType.template, - name: - timelineTabsStyle === TimelineTabsStyle.filter - ? i18n.FILTER_TIMELINES(i18n.TAB_TEMPLATES) - : i18n.TAB_TEMPLATES, + name: i18n.TAB_TEMPLATES, href: formatUrl(getTimelineTabsUrl(TimelineType.template, urlSearch)), disabled: false, - withNext: false, - count: - timelineTabsStyle === TimelineTabsStyle.filter - ? templateTimelineCount ?? undefined - : undefined, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTemplateTimeline : noop, }, ], - [ - defaultTimelineCount, - templateTimelineCount, - urlSearch, - formatUrl, - goToTimeline, - goToTemplateTimeline, - ] + [urlSearch, formatUrl, goToTimeline, goToTemplateTimeline] ); const onFilterClicked = useCallback( (tabId, tabStyle: TimelineTabsStyle) => { setTimelineTypes((prevTimelineTypes) => { - if (tabId === prevTimelineTypes && tabStyle === TimelineTabsStyle.filter) { - return tabId === TimelineType.default ? TimelineType.template : TimelineType.default; - } else if (prevTimelineTypes !== tabId) { + if (prevTimelineTypes !== tabId) { setTimelineTypes(tabId); } return prevTimelineTypes; @@ -139,21 +116,23 @@ export const useTimelineTypes = ({ }, [tabName]); const timelineFilters = useMemo(() => { - return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( - void }) => { - tab.onClick(ev); - onFilterClicked(tab.id, TimelineTabsStyle.filter); - }} - withNext={tab.withNext} - > - {tab.name} - - )); + return ( + + {getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( + void }) => { + tab.onClick(ev); + onFilterClicked(tab.id, TimelineTabsStyle.filter); + }} + > + {tab.name} + + ))} + + ); }, [timelineType, getFilterOrTabs, onFilterClicked]); return { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b8a67d9c3388e..439b9a93d0d97 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19247,7 +19247,6 @@ "xpack.securitySolution.open.timeline.exportSelectedButton": "選択した項目のエクスポート", "xpack.securitySolution.open.timeline.favoriteSelectedButton": "選択中のお気に入り", "xpack.securitySolution.open.timeline.favoritesTooltip": "お気に入り", - "xpack.securitySolution.open.timeline.filterByTimelineTypesTitle": "{timelineType}のみ", "xpack.securitySolution.open.timeline.lastModifiedTableHeader": "最終更新:", "xpack.securitySolution.open.timeline.missingSavedObjectIdTooltip": "savedObjectId がありません", "xpack.securitySolution.open.timeline.modifiedByTableHeader": "変更者:", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 229265fe62252..643192df99309 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19294,7 +19294,6 @@ "xpack.securitySolution.open.timeline.exportSelectedButton": "导出所选", "xpack.securitySolution.open.timeline.favoriteSelectedButton": "收藏所选", "xpack.securitySolution.open.timeline.favoritesTooltip": "收藏夹", - "xpack.securitySolution.open.timeline.filterByTimelineTypesTitle": "仅 {timelineType}", "xpack.securitySolution.open.timeline.lastModifiedTableHeader": "最后修改时间", "xpack.securitySolution.open.timeline.missingSavedObjectIdTooltip": "缺失 savedObjectId", "xpack.securitySolution.open.timeline.modifiedByTableHeader": "修改者", From ac18273df58305706cb0d770d038f399ab0479ea Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 9 Feb 2021 14:33:46 +0100 Subject: [PATCH 051/100] [APM] Higher timeout for flaky abort test (#90728) --- .../create_es_client/create_apm_event_client/index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts index 741f282b169ed..addd7391d782d 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts @@ -69,10 +69,10 @@ describe('createApmEventClient', () => { incomingRequest.on('abort', () => { setTimeout(() => { resolve(undefined); - }, 0); + }, 100); }); incomingRequest.abort(); - }, 50); + }, 100); }); expect(abort).toHaveBeenCalled(); From 3456766c394ba88037923223ba74193f0902ba28 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 9 Feb 2021 13:40:12 +0000 Subject: [PATCH 052/100] skip flaky suite (#90576) --- .../apps/transform/feature_controls/transform_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts index 46e0c01afcc38..b8d6b88e4ed9a 100644 --- a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts +++ b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts @@ -15,7 +15,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); - describe('security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/90576 + describe.skip('security', () => { before(async () => { await esArchiver.load('empty_kibana'); await PageObjects.security.forceLogout(); From b78f9f9c460cb8a34aad3e6d3931af3acd499fea Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 9 Feb 2021 13:42:48 +0000 Subject: [PATCH 053/100] skip flaky suite (#50448) --- x-pack/test/functional/apps/status_page/status_page.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index d2d6a24bdccd1..55a54245cf832 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -14,7 +14,8 @@ export default function statusPageFunctonalTests({ const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['security', 'statusPage', 'home']); - describe('Status Page', function () { + // FLAKY: https://github.com/elastic/kibana/issues/50448 + describe.skip('Status Page', function () { this.tags(['skipCloud', 'includeFirefox']); before(async () => await esArchiver.load('empty_kibana')); after(async () => await esArchiver.unload('empty_kibana')); From 731e333078d31b00e71c09cc5ac2bf1a6457ad96 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 9 Feb 2021 15:43:40 +0200 Subject: [PATCH 054/100] [Search Sessions] Apply awaits to avoid unhandled errors (#90593) * Apply awaits to avoid unhandled errors * catch and ignore tracking error * added reject test for search service * Improve search service api test coverage Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data/server/search/search_service.test.ts | 287 +++++++++++++++++- .../data/server/search/search_service.ts | 56 ++-- 2 files changed, 315 insertions(+), 28 deletions(-) diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index d6589e88085a0..192c133c94a04 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -7,7 +7,7 @@ */ import type { MockedKeys } from '@kbn/utility-types/jest'; -import { CoreSetup, CoreStart } from '../../../../core/server'; +import { CoreSetup, CoreStart, SavedObject } from '../../../../core/server'; import { coreMock } from '../../../../core/server/mocks'; import { DataPluginStart } from '../plugin'; @@ -86,13 +86,22 @@ describe('Search service', () => { describe('asScopedProvider', () => { let mockScopedClient: IScopedSearchClient; let searcPluginStart: ISearchStart>; - let mockStrategy: jest.Mocked; + let mockStrategy: any; + let mockStrategyNoCancel: jest.Mocked; let mockSessionService: ISearchSessionService; let mockSessionClient: jest.Mocked; const sessionId = '1234'; beforeEach(() => { - mockStrategy = { search: jest.fn().mockReturnValue(of({})) }; + mockStrategy = { + search: jest.fn().mockReturnValue(of({})), + cancel: jest.fn(), + extend: jest.fn(), + }; + + mockStrategyNoCancel = { + search: jest.fn().mockReturnValue(of({})), + }; mockSessionClient = createSearchSessionsClientMock(); mockSessionService = { @@ -104,6 +113,7 @@ describe('Search service', () => { expressions: expressionsPluginMock.createSetupContract(), }); pluginSetup.registerSearchStrategy('es', mockStrategy); + pluginSetup.registerSearchStrategy('nocancel', mockStrategyNoCancel); pluginSetup.__enhance({ defaultStrategy: 'es', sessionService: mockSessionService, @@ -123,7 +133,7 @@ describe('Search service', () => { it('searches using the original request if not restoring, trackId is not called if there is no id in the response', async () => { const searchRequest = { params: {} }; const options = { sessionId, isStored: false, isRestore: false }; - mockSessionClient.trackId = jest.fn(); + mockSessionClient.trackId = jest.fn().mockResolvedValue(undefined); mockStrategy.search.mockReturnValue( of({ @@ -165,10 +175,27 @@ describe('Search service', () => { expect(request).toStrictEqual({ ...searchRequest, id: 'my_id' }); }); + it('does not fail if `trackId` throws', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + mockSessionClient.trackId = jest.fn().mockRejectedValue(undefined); + + mockStrategy.search.mockReturnValue( + of({ + id: 'my_id', + rawResponse: {} as any, + }) + ); + + await mockScopedClient.search(searchRequest, options).toPromise(); + + expect(mockSessionClient.trackId).toBeCalledTimes(1); + }); + it('calls `trackId` for every response, if the response contains an `id` and not restoring', async () => { const searchRequest = { params: {} }; const options = { sessionId, isStored: false, isRestore: false }; - mockSessionClient.trackId = jest.fn(); + mockSessionClient.trackId = jest.fn().mockResolvedValue(undefined); mockStrategy.search.mockReturnValue( of( @@ -195,7 +222,7 @@ describe('Search service', () => { const searchRequest = { params: {} }; const options = { sessionId, isStored: true, isRestore: true }; mockSessionClient.getId = jest.fn().mockResolvedValueOnce('my_id'); - mockSessionClient.trackId = jest.fn(); + mockSessionClient.trackId = jest.fn().mockResolvedValue(undefined); await mockScopedClient.search(searchRequest, options).toPromise(); @@ -206,12 +233,258 @@ describe('Search service', () => { const searchRequest = { params: {} }; const options = {}; mockSessionClient.getId = jest.fn().mockResolvedValueOnce('my_id'); - mockSessionClient.trackId = jest.fn(); + mockSessionClient.trackId = jest.fn().mockResolvedValue(undefined); await mockScopedClient.search(searchRequest, options).toPromise(); expect(mockSessionClient.trackId).not.toBeCalled(); }); }); + + describe('cancelSession', () => { + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: 'search-session', + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + }, + references: [], + }; + + it('cancels a saved object with no search ids', async () => { + mockSessionClient.getSearchIdMapping = jest + .fn() + .mockResolvedValue(new Map()); + mockSessionClient.cancel = jest.fn().mockResolvedValue(mockSavedObject); + const cancelSpy = jest.spyOn(mockScopedClient, 'cancel'); + + await mockScopedClient.cancelSession('123'); + + expect(mockSessionClient.cancel).toHaveBeenCalledTimes(1); + expect(cancelSpy).not.toHaveBeenCalled(); + }); + + it('cancels a saved object and search ids', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockStrategy.cancel = jest.fn(); + mockSessionClient.cancel = jest.fn().mockResolvedValue(mockSavedObject); + + await mockScopedClient.cancelSession('123'); + + expect(mockSessionClient.cancel).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('abc'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('cancels a saved object with some strategies that dont support cancellation, dont throw an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'nocancel'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockStrategy.cancel = jest.fn(); + mockSessionClient.cancel = jest.fn().mockResolvedValue(mockSavedObject); + + await mockScopedClient.cancelSession('123'); + + expect(mockSessionClient.cancel).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('def'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('cancels a saved object with some strategies that dont exist, dont throw an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'notsupported'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockStrategy.cancel = jest.fn(); + mockSessionClient.cancel = jest.fn().mockResolvedValue(mockSavedObject); + + await mockScopedClient.cancelSession('123'); + + expect(mockSessionClient.cancel).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('def'); + expect(options).toHaveProperty('strategy', 'es'); + }); + }); + + describe('deleteSession', () => { + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: 'search-session', + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + }, + references: [], + }; + + it('deletes a saved object with no search ids', async () => { + mockSessionClient.getSearchIdMapping = jest + .fn() + .mockResolvedValue(new Map()); + mockSessionClient.delete = jest.fn().mockResolvedValue(mockSavedObject); + const cancelSpy = jest.spyOn(mockScopedClient, 'cancel'); + + await mockScopedClient.deleteSession('123'); + + expect(mockSessionClient.delete).toHaveBeenCalledTimes(1); + expect(cancelSpy).not.toHaveBeenCalled(); + }); + + it('deletes a saved object and search ids', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockSessionClient.delete = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.cancel = jest.fn(); + + await mockScopedClient.deleteSession('123'); + + expect(mockSessionClient.delete).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('abc'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('deletes a saved object with some strategies that dont support cancellation, dont throw an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'nocancel'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockSessionClient.delete = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.cancel = jest.fn(); + + await mockScopedClient.deleteSession('123'); + + expect(mockSessionClient.delete).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('def'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('deletes a saved object with some strategies that dont exist, dont throw an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'notsupported'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockStrategy.cancel = jest.fn(); + mockSessionClient.delete = jest.fn().mockResolvedValue(mockSavedObject); + + await mockScopedClient.deleteSession('123'); + + expect(mockSessionClient.delete).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('def'); + expect(options).toHaveProperty('strategy', 'es'); + }); + }); + + describe('extendSession', () => { + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: 'search-session', + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + }, + references: [], + }; + + it('extends a saved object with no search ids', async () => { + mockSessionClient.getSearchIdMapping = jest + .fn() + .mockResolvedValue(new Map()); + mockSessionClient.extend = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.extend = jest.fn(); + + await mockScopedClient.extendSession('123', new Date('2020-01-01')); + + expect(mockSessionClient.extend).toHaveBeenCalledTimes(1); + expect(mockStrategy.extend).not.toHaveBeenCalled(); + }); + + it('extends a saved object and search ids', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockSessionClient.extend = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.extend = jest.fn(); + + await mockScopedClient.extendSession('123', new Date('2020-01-01')); + + expect(mockSessionClient.extend).toHaveBeenCalledTimes(1); + expect(mockStrategy.extend).toHaveBeenCalledTimes(1); + const [searchId, keepAlive, options] = mockStrategy.extend.mock.calls[0]; + expect(searchId).toBe('abc'); + expect(keepAlive).toContain('ms'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('doesnt extend the saved object with some strategies that dont support cancellation, throws an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'nocancel'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockSessionClient.extend = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.extend = jest.fn().mockResolvedValue({}); + + const extendRes = mockScopedClient.extendSession('123', new Date('2020-01-01')); + + await expect(extendRes).rejects.toThrowError( + 'Failed to extend the expiration of some searches' + ); + + expect(mockSessionClient.extend).not.toHaveBeenCalled(); + const [searchId, keepAlive, options] = mockStrategy.extend.mock.calls[0]; + expect(searchId).toBe('def'); + expect(keepAlive).toContain('ms'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('doesnt extend the saved object with some strategies that dont exist, throws an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'notsupported'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockSessionClient.extend = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.extend = jest.fn().mockResolvedValue({}); + + const extendRes = mockScopedClient.extendSession('123', new Date('2020-01-01')); + + await expect(extendRes).rejects.toThrowError( + 'Failed to extend the expiration of some searches' + ); + + expect(mockSessionClient.extend).not.toHaveBeenCalled(); + const [searchId, keepAlive, options] = mockStrategy.extend.mock.calls[0]; + expect(searchId).toBe('def'); + expect(keepAlive).toContain('ms'); + expect(options).toHaveProperty('strategy', 'es'); + }); + }); }); }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index ce0771a1e9df8..6ece8ff945468 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -275,7 +275,10 @@ export class SearchService implements Plugin { switchMap((searchRequest) => strategy.search(searchRequest, options, deps)), tap((response) => { if (!options.sessionId || !response.id || options.isRestore) return; - deps.searchSessionsClient.trackId(request, response.id, options); + // intentionally swallow tracking error, as it shouldn't fail the search + deps.searchSessionsClient.trackId(request, response.id, options).catch((trackErr) => { + this.logger.error(trackErr); + }); }) ); } catch (e) { @@ -283,7 +286,11 @@ export class SearchService implements Plugin { } }; - private cancel = (deps: SearchStrategyDependencies, id: string, options: ISearchOptions = {}) => { + private cancel = async ( + deps: SearchStrategyDependencies, + id: string, + options: ISearchOptions = {} + ) => { const strategy = this.getSearchStrategy(options.strategy); if (!strategy.cancel) { throw new KbnServerError( @@ -294,7 +301,7 @@ export class SearchService implements Plugin { return strategy.cancel(id, options, deps); }; - private extend = ( + private extend = async ( deps: SearchStrategyDependencies, id: string, keepAlive: string, @@ -309,25 +316,26 @@ export class SearchService implements Plugin { private cancelSessionSearches = async (deps: SearchStrategyDependencies, sessionId: string) => { const searchIdMapping = await deps.searchSessionsClient.getSearchIdMapping(sessionId); - - for (const [searchId, strategyName] of searchIdMapping.entries()) { - const searchOptions = { - sessionId, - strategy: strategyName, - isStored: true, - }; - this.cancel(deps, searchId, searchOptions); - } + await Promise.allSettled( + Array.from(searchIdMapping).map(([searchId, strategyName]) => { + const searchOptions = { + sessionId, + strategy: strategyName, + isStored: true, + }; + return this.cancel(deps, searchId, searchOptions); + }) + ); }; private cancelSession = async (deps: SearchStrategyDependencies, sessionId: string) => { const response = await deps.searchSessionsClient.cancel(sessionId); - this.cancelSessionSearches(deps, sessionId); + await this.cancelSessionSearches(deps, sessionId); return response; }; private deleteSession = async (deps: SearchStrategyDependencies, sessionId: string) => { - this.cancelSessionSearches(deps, sessionId); + await this.cancelSessionSearches(deps, sessionId); return deps.searchSessionsClient.delete(sessionId); }; @@ -339,13 +347,19 @@ export class SearchService implements Plugin { const searchIdMapping = await deps.searchSessionsClient.getSearchIdMapping(sessionId); const keepAlive = `${moment(expires).diff(moment())}ms`; - for (const [searchId, strategyName] of searchIdMapping.entries()) { - const searchOptions = { - sessionId, - strategy: strategyName, - isStored: true, - }; - await this.extend(deps, searchId, keepAlive, searchOptions); + const result = await Promise.allSettled( + Array.from(searchIdMapping).map(([searchId, strategyName]) => { + const searchOptions = { + sessionId, + strategy: strategyName, + isStored: true, + }; + return this.extend(deps, searchId, keepAlive, searchOptions); + }) + ); + + if (result.some((extRes) => extRes.status === 'rejected')) { + throw new Error('Failed to extend the expiration of some searches'); } return deps.searchSessionsClient.extend(sessionId, expires); From 810e4ab8e8206949965c89c889aac2fc396c4111 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 9 Feb 2021 08:54:51 -0500 Subject: [PATCH 055/100] [Fleet] Prevent agents from enrolling in a managed policy (#90458) ## Summary Add guard to `/agents/enroll` API preventing agents from enrolling in managed policies closes #90435 - [x] No Agents can be enrolled into this policy by the user. - [x] The install & enroll commands should print an error to the console if the enroll command fails (due to being a managed policy or any other reason) #### So how do you associate an agent with a managed policy? Enroll in an unmanaged policy then set that policy to managed. We don't restrict the agent policy, only what other things (agents, integrations) can do if they're associated with a managed policy. A _force flag_ has been mentioned for some other actions. It might work here as well, but I'd like to handle discussion & implementation of those later. ### Manual testing
Prevent enroll for managed policies 1. Created a managed agent policy ``` curl --user elastic:changeme -X POST localhost:5601/api/fleet/agent_policies -H 'Content-Type: application/json' -d'{ "name": "User created MANAGED", "namespace": "default", "is_managed": true}' -H 'kbn-xsrf: true' {"item":{"id":"3bd07db0-67d0-11eb-b656-21ad68ebfa8a","name":"User created MANAGED","namespace":"default","is_managed":true,"revision":1,"updated_at":"2021-02-05T16:36:01.931Z","updated_by":"elastic"}} ``` 2. Try `install` command show in the UI ``` sudo ./elastic-agent install -f --kibana-url=http://localhost:5601 --enrollment-token=WmcwTWMzY0IzWlBUUWJJUjZqRDA6UGRZelVlaS1STml1cVdjSUVwSkJRQQ== --insecure Password: The Elastic Agent is currently in BETA and should not be used in production Error: fail to enroll: fail to execute request to Kibana: Status code: 400, Kibana returned an error: Bad Request, message: Cannot enroll in managed policy 3bd07db0-67d0-11eb-b656-21ad68ebfa8a Error: enroll command failed with exit code: 1 ``` 3. Observe `Cannot enroll in managed policy 3bd07db0-67d0-11eb-b656-21ad68ebfa8a` error 4. Try `enroll` instead: ``` sudo ./elastic-agent enroll http://localhost:5601 WmcwTWMzY0IzWlBUUWJJUjZqRDA6UGRZelVlaS1STml1cVdjSUVwSkJRQQ== --insecure The Elastic Agent is currently in BETA and should not be used in production This will replace your current settings. Do you want to continue? [Y/n]: Error: fail to enroll: fail to execute request to Kibana: Status code: 400, Kibana returned an error: Bad Request, message: Cannot enroll in managed policy 3bd07db0-67d0-11eb-b656-21ad68ebfa8a ``` 5. Observe same `Cannot enroll in managed policy 3bd07db0-67d0-11eb-b656-21ad68ebfa8a` error
Enroll in unmanaged policy, then update it to managed Agent policies are `is_managed: false` by default, or we can update the policy to `is_managed: false`, like: ``` curl --user elastic:changeme -X PUT localhost:5601/api/fleet/agent_policies/3bd07db0-67d0-11eb-b656-21ad68ebfa8a -H 'Content-Type: application/json' -d'{ "is_managed": false, "name": "xyz", "namespace": "default" }' -H 'kbn-xsrf: true' {"item":{"id":"3bd07db0-67d0-11eb-b656-21ad68ebfa8a","name":"xyz","namespace":"default","is_managed":false,"revision":4,"updated_at":"2021-02-05T17:42:05.610Z","updated_by":"elastic","package_policies":[]}} ``` then enroll ``` sudo ./elastic-agent install -f --kibana-url=http://localhost:5601 --enrollment-token=WmcwTWMzY0IzWlBUUWJJUjZqRDA6UGRZelVlaS1STml1cVdjSUVwSkJRQQ== --insecure The Elastic Agent is currently in BETA and should not be used in production Successfully enrolled the Elastic Agent. Installation was successful and Elastic Agent is running. ``` and set the policy back to managed ``` curl --user elastic:changeme -X PUT localhost:5601/api/fleet/agent_policies/3bd07db0-67d0-11eb-b656-21ad68ebfa8a -H 'Content-Type: application/json' -d'{ "is_managed": true, "name": "xyz", "namespace": "default" }' -H 'kbn-xsrf: true' {"item":{"id":"3bd07db0-67d0-11eb-b656-21ad68ebfa8a","name":"xyz","namespace":"default","is_managed":true,"revision":5,"updated_at":"2021-02-05T17:44:18.757Z","updated_by":"elastic","package_policies":[]}} ``` with all the restrictions that entails (cannot unenroll, reassign, etc) ``` curl --user elastic:changeme -X PUT 'http://localhost:5601/api/fleet/agents/8169f0a0-67d9-11eb-80f2-73dd45e7318e/reassign' -X 'PUT' -H 'kbn-xsrf: abc' -H 'Content-Type: application/json' --data-raw '{"policy_id":"729f8440-67cf-11eb-b656-21ad68ebfa8a"}' { "statusCode": 400, "error": "Bad Request", "message": "Cannot reassign an agent from managed agent policy 3bd07db0-67d0-11eb-b656-21ad68ebfa8a" } ```
### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../fleet/server/services/agents/enroll.ts | 11 ++++- .../apis/agents/enroll.ts | 48 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/enroll.ts b/x-pack/plugins/fleet/server/services/agents/enroll.ts index c984a84ceea01..6ca19bf884cca 100644 --- a/x-pack/plugins/fleet/server/services/agents/enroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/enroll.ts @@ -11,11 +11,13 @@ import semverParse from 'semver/functions/parse'; import semverDiff from 'semver/functions/diff'; import semverLte from 'semver/functions/lte'; -import { SavedObjectsClientContract } from 'src/core/server'; -import { AgentType, Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; +import type { SavedObjectsClientContract } from 'src/core/server'; +import type { AgentType, Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; import { savedObjectToAgent } from './saved_objects'; import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; +import { IngestManagerError } from '../../errors'; import * as APIKeyService from '../api_keys'; +import { agentPolicyService } from '../../services'; import { appContextService } from '../app_context'; export async function enroll( @@ -27,6 +29,11 @@ export async function enroll( const agentVersion = metadata?.local?.elastic?.agent?.version; validateAgentVersion(agentVersion); + const agentPolicy = await agentPolicyService.get(soClient, agentPolicyId, false); + if (agentPolicy?.is_managed) { + throw new IngestManagerError(`Cannot enroll in managed policy ${agentPolicyId}`); + } + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { const esClient = appContextService.getInternalUserESClient(); diff --git a/x-pack/test/fleet_api_integration/apis/agents/enroll.ts b/x-pack/test/fleet_api_integration/apis/agents/enroll.ts index 96c472697801e..3358d045fe69b 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/enroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/enroll.ts @@ -18,8 +18,9 @@ export default function (providerContext: FtrProviderContext) { const esArchiver = getService('esArchiver'); const esClient = getService('es'); const kibanaServer = getService('kibanaServer'); - + const supertestWithAuth = getService('supertest'); const supertest = getSupertestWithoutAuth(providerContext); + let apiKey: { id: string; api_key: string }; let kibanaVersion: string; @@ -58,6 +59,51 @@ export default function (providerContext: FtrProviderContext) { await esArchiver.unload('fleet/agents'); }); + it('should not allow enrolling in a managed policy', async () => { + // update existing policy to managed + await supertestWithAuth + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + is_managed: true, + }) + .expect(200); + + // try to enroll in managed policy + const { body } = await supertest + .post(`/api/fleet/agents/enroll`) + .set('kbn-xsrf', 'xxx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + type: 'PERMANENT', + metadata: { + local: { + elastic: { agent: { version: kibanaVersion } }, + }, + user_provided: {}, + }, + }) + .expect(400); + + expect(body.message).to.contain('Cannot enroll in managed policy'); + + // restore to original (unmanaged) + await supertestWithAuth + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + is_managed: false, + }) + .expect(200); + }); + it('should not allow to enroll an agent with a invalid enrollment', async () => { await supertest .post(`/api/fleet/agents/enroll`) From 82d1672e79c4a90dbabf11be605de3ed910592db Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 9 Feb 2021 15:38:35 +0100 Subject: [PATCH 056/100] [Core] add timeout for "stop" lifecycle (#90432) * add timeout for stop lifecycle * add timeout for stop lifecycle * update message * cleanup timeout to remove async tasks --- packages/kbn-std/src/promise.test.ts | 22 ++++------ packages/kbn-std/src/promise.ts | 27 +++++++----- src/core/public/plugins/plugins_service.ts | 26 +++++++++--- .../server/plugins/plugins_system.test.ts | 42 +++++++++++++++++++ src/core/server/plugins/plugins_system.ts | 36 ++++++++++++---- 5 files changed, 117 insertions(+), 36 deletions(-) diff --git a/packages/kbn-std/src/promise.test.ts b/packages/kbn-std/src/promise.test.ts index f7c119acd0c7a..bf4f3951d5850 100644 --- a/packages/kbn-std/src/promise.test.ts +++ b/packages/kbn-std/src/promise.test.ts @@ -12,40 +12,36 @@ const delay = (ms: number, resolveValue?: any) => new Promise((resolve) => setTimeout(resolve, ms, resolveValue)); describe('withTimeout', () => { - it('resolves with a promise value if resolved in given timeout', async () => { + it('resolves with a promise value and "timedout: false" if resolved in given timeout', async () => { await expect( withTimeout({ promise: delay(10, 'value'), - timeout: 200, - errorMessage: 'error-message', + timeoutMs: 200, }) - ).resolves.toBe('value'); + ).resolves.toStrictEqual({ value: 'value', timedout: false }); }); - it('rejects with errorMessage if not resolved in given time', async () => { + it('resolves with "timedout: false" if not resolved in given time', async () => { await expect( withTimeout({ promise: delay(200, 'value'), - timeout: 10, - errorMessage: 'error-message', + timeoutMs: 10, }) - ).rejects.toMatchInlineSnapshot(`[Error: error-message]`); + ).resolves.toStrictEqual({ timedout: true }); await expect( withTimeout({ promise: new Promise((i) => i), - timeout: 10, - errorMessage: 'error-message', + timeoutMs: 10, }) - ).rejects.toMatchInlineSnapshot(`[Error: error-message]`); + ).resolves.toStrictEqual({ timedout: true }); }); it('does not swallow promise error', async () => { await expect( withTimeout({ promise: Promise.reject(new Error('from-promise')), - timeout: 10, - errorMessage: 'error-message', + timeoutMs: 10, }) ).rejects.toMatchInlineSnapshot(`[Error: from-promise]`); }); diff --git a/packages/kbn-std/src/promise.ts b/packages/kbn-std/src/promise.ts index 9d8f7703c026d..9209c2ce372c6 100644 --- a/packages/kbn-std/src/promise.ts +++ b/packages/kbn-std/src/promise.ts @@ -6,19 +6,26 @@ * Side Public License, v 1. */ -export function withTimeout({ +export async function withTimeout({ promise, - timeout, - errorMessage, + timeoutMs, }: { promise: Promise; - timeout: number; - errorMessage: string; -}) { - return Promise.race([ - promise, - new Promise((resolve, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout)), - ]) as Promise; + timeoutMs: number; +}): Promise<{ timedout: true } | { timedout: false; value: T }> { + let timeout: NodeJS.Timeout | undefined; + try { + return (await Promise.race([ + promise.then((v) => ({ value: v, timedout: false })), + new Promise((resolve) => { + timeout = setTimeout(() => resolve({ timedout: true }), timeoutMs); + }), + ])) as Promise<{ timedout: true } | { timedout: false; value: T }>; + } finally { + if (timeout !== undefined) { + clearTimeout(timeout); + } + } } export function isPromise(maybePromise: T | Promise): maybePromise is Promise { diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 57fbe4cbecd12..230a675b4cda6 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -111,11 +111,18 @@ export class PluginsService implements CoreService { `); }); }); + +describe('stop', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('waits for 30 sec to finish "stop" and move on to the next plugin.', async () => { + const [plugin1, plugin2] = [createPlugin('timeout-stop-1'), createPlugin('timeout-stop-2')].map( + (plugin, index) => { + jest.spyOn(plugin, 'setup').mockResolvedValue(`setup-as-${index}`); + jest.spyOn(plugin, 'start').mockResolvedValue(`started-as-${index}`); + pluginsSystem.addPlugin(plugin); + return plugin; + } + ); + + const stopSpy1 = jest + .spyOn(plugin1, 'stop') + .mockImplementationOnce(() => new Promise((resolve) => resolve)); + const stopSpy2 = jest.spyOn(plugin2, 'stop').mockImplementationOnce(() => Promise.resolve()); + + mockCreatePluginSetupContext.mockImplementation(() => ({})); + + await pluginsSystem.setupPlugins(setupDeps); + const stopPromise = pluginsSystem.stopPlugins(); + + jest.runAllTimers(); + await stopPromise; + expect(stopSpy1).toHaveBeenCalledTimes(1); + expect(stopSpy2).toHaveBeenCalledTimes(1); + + expect(loggingSystemMock.collect(logger).warn.flat()).toEqual( + expect.arrayContaining([ + `"timeout-stop-1" plugin didn't stop in 30sec., move on to the next.`, + ]) + ); + }); +}); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index b7b8c297ea571..0244254838fab 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -105,11 +105,18 @@ export class PluginsSystem { `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` ); } - contract = await withTimeout({ + const contractMaybe = await withTimeout({ promise: contractOrPromise, - timeout: 10 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + timeoutMs: 10 * Sec, }); + + if (contractMaybe.timedout) { + throw new Error( + `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.` + ); + } else { + contract = contractMaybe.value; + } } else { contract = contractOrPromise; } @@ -154,11 +161,18 @@ export class PluginsSystem { `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` ); } - contract = await withTimeout({ + const contractMaybe = await withTimeout({ promise: contractOrPromise, - timeout: 10 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + timeoutMs: 10 * Sec, }); + + if (contractMaybe.timedout) { + throw new Error( + `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.` + ); + } else { + contract = contractMaybe.value; + } } else { contract = contractOrPromise; } @@ -181,7 +195,15 @@ export class PluginsSystem { const pluginName = this.satupPlugins.pop()!; this.log.debug(`Stopping plugin "${pluginName}"...`); - await this.plugins.get(pluginName)!.stop(); + + const resultMaybe = await withTimeout({ + promise: this.plugins.get(pluginName)!.stop(), + timeoutMs: 30 * Sec, + }); + + if (resultMaybe?.timedout) { + this.log.warn(`"${pluginName}" plugin didn't stop in 30sec., move on to the next.`); + } } } From 9314b8e2fadf78b99ecbcd8588d2507076b42563 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 9 Feb 2021 07:57:00 -0700 Subject: [PATCH 057/100] [Maps] use chart pallete registry to support sync colors in dashboard (#88099) * [Maps] use chart pallete registry to support sync colors in dashboard * pass getColor to createLayerInstance * use chartsPaletteServiceGetColor to get categorical color * revert changes to layer_actions * tslint and jest test updates Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/kibana.json | 3 +- .../blended_vector_layer.ts | 8 ++++- .../layers/vector_layer/vector_layer.tsx | 15 ++++++-- .../properties/dynamic_color_property.tsx | 35 ++++++++++++++++--- .../classes/styles/vector/vector_style.tsx | 21 +++++++---- .../maps/public/embeddable/map_embeddable.tsx | 28 ++++++++++++++- x-pack/plugins/maps/public/kibana_services.ts | 20 +++++++++++ x-pack/plugins/maps/public/plugin.ts | 2 ++ .../reducers/non_serializable_instances.d.ts | 9 +++++ .../reducers/non_serializable_instances.js | 20 +++++++++++ x-pack/plugins/maps/public/reducers/store.js | 1 + .../public/selectors/map_selectors.test.ts | 5 --- .../maps/public/selectors/map_selectors.ts | 15 +++++--- 13 files changed, 156 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 744cc18c36f3e..3966af9e28742 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -25,7 +25,8 @@ ], "optionalPlugins": [ "home", - "savedObjectsTagging" + "savedObjectsTagging", + "charts" ], "ui": true, "server": true, diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index efd022292f90b..5d4b915c4e971 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -169,6 +169,7 @@ function getClusterStyleDescriptor( } export interface BlendedVectorLayerArguments { + chartsPaletteServiceGetColor?: (value: string) => string | null; source: IVectorSource; layerDescriptor: VectorLayerDescriptor; } @@ -205,7 +206,12 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { this._documentStyle, this._clusterSource ); - this._clusterStyle = new VectorStyle(clusterStyleDescriptor, this._clusterSource, this); + this._clusterStyle = new VectorStyle( + clusterStyleDescriptor, + this._clusterSource, + this, + options.chartsPaletteServiceGetColor + ); let isClustered = false; const countDataRequest = this.getDataRequest(ACTIVE_COUNT_DATA_ID); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index ee1cda6eaee43..e9c0cb29c7c17 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -81,6 +81,7 @@ export interface VectorLayerArguments { source: IVectorSource; joins?: InnerJoin[]; layerDescriptor: VectorLayerDescriptor; + chartsPaletteServiceGetColor?: (value: string) => string | null; } export interface IVectorLayer extends ILayer { @@ -119,13 +120,23 @@ export class VectorLayer extends AbstractLayer { return layerDescriptor as VectorLayerDescriptor; } - constructor({ layerDescriptor, source, joins = [] }: VectorLayerArguments) { + constructor({ + layerDescriptor, + source, + joins = [], + chartsPaletteServiceGetColor, + }: VectorLayerArguments) { super({ layerDescriptor, source, }); this._joins = joins; - this._style = new VectorStyle(layerDescriptor.style, source, this); + this._style = new VectorStyle( + layerDescriptor.style, + source, + this, + chartsPaletteServiceGetColor + ); } getSource(): IVectorSource { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx index cac56ad1c8a57..d654cdc6bff51 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx @@ -16,7 +16,12 @@ import { getPercentilesMbColorRampStops, getColorPalette, } from '../../color_palettes'; -import { COLOR_MAP_TYPE, DATA_MAPPING_FUNCTION } from '../../../../../common/constants'; +import { + COLOR_MAP_TYPE, + DATA_MAPPING_FUNCTION, + FieldFormatter, + VECTOR_STYLES, +} from '../../../../../common/constants'; import { isCategoricalStopsInvalid, getOtherCategoryLabel, @@ -26,6 +31,8 @@ import { Break, BreakedLegend } from '../components/legend/breaked_legend'; import { ColorDynamicOptions, OrdinalColorStop } from '../../../../../common/descriptor_types'; import { LegendProps } from './style_property'; import { getOrdinalSuffix } from '../../../util/ordinal_suffix'; +import { IField } from '../../../fields/field'; +import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; const UP_TO = i18n.translate('xpack.maps.legend.upto', { defaultMessage: 'up to', @@ -34,6 +41,20 @@ const EMPTY_STOPS = { stops: [], defaultColor: null }; const RGBA_0000 = 'rgba(0,0,0,0)'; export class DynamicColorProperty extends DynamicStyleProperty { + private readonly _chartsPaletteServiceGetColor?: (value: string) => string | null; + + constructor( + options: ColorDynamicOptions, + styleName: VECTOR_STYLES, + field: IField | null, + vectorLayer: IVectorLayer, + getFieldFormatter: (fieldName: string) => null | FieldFormatter, + chartsPaletteServiceGetColor?: (value: string) => string | null + ) { + super(options, styleName, field, vectorLayer, getFieldFormatter); + this._chartsPaletteServiceGetColor = chartsPaletteServiceGetColor; + } + syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { const color = this._getMbColor(); mbMap.setPaintProperty(mbLayerId, 'circle-color', color); @@ -260,12 +281,16 @@ export class DynamicColorProperty extends DynamicStyleProperty { - if (stop !== null) { + stops.forEach(({ stop, color }: { stop: string | number | null; color: string | null }) => { + if (stop !== null && color != null) { breaks.push({ color, symbolId, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index cef5f5048e9af..c61e72807224a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -178,7 +178,8 @@ export class VectorStyle implements IVectorStyle { constructor( descriptor: VectorStyleDescriptor | null, source: IVectorSource, - layer: IVectorLayer + layer: IVectorLayer, + chartsPaletteServiceGetColor?: (value: string) => string | null ) { this._source = source; this._layer = layer; @@ -197,11 +198,13 @@ export class VectorStyle implements IVectorStyle { ); this._lineColorStyleProperty = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], - VECTOR_STYLES.LINE_COLOR + VECTOR_STYLES.LINE_COLOR, + chartsPaletteServiceGetColor ); this._fillColorStyleProperty = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.FILL_COLOR], - VECTOR_STYLES.FILL_COLOR + VECTOR_STYLES.FILL_COLOR, + chartsPaletteServiceGetColor ); this._lineWidthStyleProperty = this._makeSizeProperty( this._descriptor.properties[VECTOR_STYLES.LINE_WIDTH], @@ -230,11 +233,13 @@ export class VectorStyle implements IVectorStyle { ); this._labelColorStyleProperty = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_COLOR], - VECTOR_STYLES.LABEL_COLOR + VECTOR_STYLES.LABEL_COLOR, + chartsPaletteServiceGetColor ); this._labelBorderColorStyleProperty = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR], - VECTOR_STYLES.LABEL_BORDER_COLOR + VECTOR_STYLES.LABEL_BORDER_COLOR, + chartsPaletteServiceGetColor ); this._labelBorderSizeStyleProperty = new LabelBorderSizeProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_SIZE].options, @@ -890,7 +895,8 @@ export class VectorStyle implements IVectorStyle { _makeColorProperty( descriptor: ColorStylePropertyDescriptor | undefined, - styleName: VECTOR_STYLES + styleName: VECTOR_STYLES, + chartsPaletteServiceGetColor?: (value: string) => string | null ) { if (!descriptor || !descriptor.options) { return new StaticColorProperty({ color: '' }, styleName); @@ -904,7 +910,8 @@ export class VectorStyle implements IVectorStyle { styleName, field, this._layer, - this._getFieldFormatter + this._getFieldFormatter, + chartsPaletteServiceGetColor ); } else { throw new Error(`${descriptor} not implemented`); diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index a1d65bf08c458..b769ac489f565 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -37,6 +37,7 @@ import { import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; import { getInspectorAdapters, + setChartsPaletteServiceGetColor, setEventHandlers, EventHandlers, } from '../reducers/non_serializable_instances'; @@ -54,7 +55,12 @@ import { RawValue, } from '../../common/constants'; import { RenderToolTipContent } from '../classes/tooltips/tooltip_property'; -import { getUiActions, getCoreI18n, getHttp } from '../kibana_services'; +import { + getUiActions, + getCoreI18n, + getHttp, + getChartsPaletteServiceGetColor, +} from '../kibana_services'; import { LayerDescriptor } from '../../common/descriptor_types'; import { MapContainer } from '../connected_components/map_container'; import { SavedMap } from '../routes/map_page'; @@ -83,6 +89,7 @@ export class MapEmbeddable private _prevQuery?: Query; private _prevRefreshConfig?: RefreshInterval; private _prevFilters?: Filter[]; + private _prevSyncColors?: boolean; private _prevSearchSessionId?: string; private _domNode?: HTMLElement; private _unsubscribeFromStore?: Unsubscribe; @@ -126,6 +133,8 @@ export class MapEmbeddable } private _initializeStore() { + this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors); + const store = this._savedMap.getStore(); store.dispatch(setReadOnly(true)); store.dispatch(disableScrollZoom()); @@ -221,6 +230,10 @@ export class MapEmbeddable if (this.input.refreshConfig && !_.isEqual(this.input.refreshConfig, this._prevRefreshConfig)) { this._dispatchSetRefreshConfig(this.input.refreshConfig); } + + if (this.input.syncColors !== this._prevSyncColors) { + this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors); + } } _dispatchSetQuery({ @@ -261,6 +274,19 @@ export class MapEmbeddable ); } + async _dispatchSetChartsPaletteServiceGetColor(syncColors?: boolean) { + this._prevSyncColors = syncColors; + const chartsPaletteServiceGetColor = syncColors + ? await getChartsPaletteServiceGetColor() + : null; + if (syncColors !== this._prevSyncColors) { + return; + } + this._savedMap + .getStore() + .dispatch(setChartsPaletteServiceGetColor(chartsPaletteServiceGetColor)); + } + /** * * @param {HTMLElement} domNode diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 632a5f5382f73..4a7bccb31380d 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -11,6 +11,7 @@ import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; import { MapsConfigType } from '../config'; import { MapsPluginStartDependencies } from './plugin'; import { EMSSettings } from '../common/ems_settings'; +import { PaletteRegistry } from '../../../../src/plugins/charts/public'; let kibanaVersion: string; export const setKibanaVersion = (version: string) => (kibanaVersion = version); @@ -83,3 +84,22 @@ export const getShareService = () => pluginsStart.share; export const getIsAllowByValueEmbeddables = () => pluginsStart.dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; + +export async function getChartsPaletteServiceGetColor(): Promise< + ((value: string) => string) | null +> { + const paletteRegistry: PaletteRegistry | null = pluginsStart.charts + ? await pluginsStart.charts.palettes.getPalettes() + : null; + if (!paletteRegistry) { + return null; + } + + const paletteDefinition = paletteRegistry.get('default'); + const chartConfiguration = { syncColors: true }; + return (value: string) => { + const series = [{ name: value, rankAtDepth: 0, totalSeriesAtDepth: 1 }]; + const color = paletteDefinition.getColor(series, chartConfiguration); + return color ? color : '#3d3d3d'; + }; +} diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 8889d1d44f10f..4c668e0a2276b 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -64,6 +64,7 @@ import { } from './licensed_features'; import { EMSSettings } from '../common/ems_settings'; import { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; +import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -76,6 +77,7 @@ export interface MapsPluginSetupDependencies { } export interface MapsPluginStartDependencies { + charts: ChartsPluginStart; data: DataPublicPluginStart; embeddable: EmbeddableStart; mapsFileUpload: FileUploadStartContract; diff --git a/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts b/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts index 54a90946a5a89..9808a5e09b8ab 100644 --- a/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts +++ b/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts @@ -15,6 +15,7 @@ export type NonSerializableState = { inspectorAdapters: Adapters; cancelRequestCallbacks: Map {}>; // key is request token, value is cancel callback eventHandlers: Partial; + chartsPaletteServiceGetColor: (value: string) => string | null; }; export interface ResultMeta { @@ -58,6 +59,14 @@ export function getInspectorAdapters(state: MapStoreState): Adapters; export function getEventHandlers(state: MapStoreState): Partial; +export function getChartsPaletteServiceGetColor( + state: MapStoreState +): (value: string) => string | null; + +export function setChartsPaletteServiceGetColor( + chartsPaletteServiceGetColor: ((value: string) => string) | null +): AnyAction; + export function cancelRequest(requestToken?: symbol): AnyAction; export function registerCancelCallback(requestToken: symbol, callback: () => void): AnyAction; diff --git a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js index 46846a8df3f23..4cc4e91a308a5 100644 --- a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js +++ b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js @@ -12,6 +12,7 @@ import { getShowMapsInspectorAdapter } from '../kibana_services'; const REGISTER_CANCEL_CALLBACK = 'REGISTER_CANCEL_CALLBACK'; const UNREGISTER_CANCEL_CALLBACK = 'UNREGISTER_CANCEL_CALLBACK'; const SET_EVENT_HANDLERS = 'SET_EVENT_HANDLERS'; +const SET_CHARTS_PALETTE_SERVICE_GET_COLOR = 'SET_CHARTS_PALETTE_SERVICE_GET_COLOR'; function createInspectorAdapters() { const inspectorAdapters = { @@ -30,6 +31,7 @@ export function nonSerializableInstances(state, action = {}) { inspectorAdapters: createInspectorAdapters(), cancelRequestCallbacks: new Map(), // key is request token, value is cancel callback eventHandlers: {}, + chartsPaletteServiceGetColor: null, }; } @@ -50,6 +52,12 @@ export function nonSerializableInstances(state, action = {}) { eventHandlers: action.eventHandlers, }; } + case SET_CHARTS_PALETTE_SERVICE_GET_COLOR: { + return { + ...state, + chartsPaletteServiceGetColor: action.chartsPaletteServiceGetColor, + }; + } default: return state; } @@ -68,6 +76,11 @@ export const getEventHandlers = ({ nonSerializableInstances }) => { return nonSerializableInstances.eventHandlers; }; +export function getChartsPaletteServiceGetColor({ nonSerializableInstances }) { + console.log('getChartsPaletteServiceGetColor', nonSerializableInstances); + return nonSerializableInstances.chartsPaletteServiceGetColor; +} + // Actions export const registerCancelCallback = (requestToken, callback) => { return { @@ -104,3 +117,10 @@ export const setEventHandlers = (eventHandlers = {}) => { eventHandlers, }; }; + +export function setChartsPaletteServiceGetColor(chartsPaletteServiceGetColor) { + return { + type: SET_CHARTS_PALETTE_SERVICE_GET_COLOR, + chartsPaletteServiceGetColor, + }; +} diff --git a/x-pack/plugins/maps/public/reducers/store.js b/x-pack/plugins/maps/public/reducers/store.js index 3c9b5d1b98e29..4e355add59fee 100644 --- a/x-pack/plugins/maps/public/reducers/store.js +++ b/x-pack/plugins/maps/public/reducers/store.js @@ -15,6 +15,7 @@ import { MAP_DESTROYED } from '../actions'; export const DEFAULT_MAP_STORE_STATE = { ui: { ...DEFAULT_MAP_UI_STATE }, map: { ...DEFAULT_MAP_STATE }, + nonSerializableInstances: {}, }; export function createMapStore() { diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts index eb11ee61d9deb..dd6a9fc377e5b 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts @@ -11,11 +11,6 @@ jest.mock('../classes/layers/blended_vector_layer/blended_vector_layer', () => { jest.mock('../classes/layers/heatmap_layer/heatmap_layer', () => {}); jest.mock('../classes/layers/vector_tile_layer/vector_tile_layer', () => {}); jest.mock('../classes/joins/inner_join', () => {}); -jest.mock('../reducers/non_serializable_instances', () => ({ - getInspectorAdapters: () => { - return {}; - }, -})); jest.mock('../kibana_services', () => ({ getTimeFilter: () => ({ getTime: () => { diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 34af789f6834f..27281fe17f0fa 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -18,7 +18,10 @@ import { VectorStyle } from '../classes/styles/vector/vector_style'; import { HeatmapLayer } from '../classes/layers/heatmap_layer/heatmap_layer'; import { BlendedVectorLayer } from '../classes/layers/blended_vector_layer/blended_vector_layer'; import { getTimeFilter } from '../kibana_services'; -import { getInspectorAdapters } from '../reducers/non_serializable_instances'; +import { + getChartsPaletteServiceGetColor, + getInspectorAdapters, +} from '../reducers/non_serializable_instances'; import { TiledVectorLayer } from '../classes/layers/tiled_vector_layer/tiled_vector_layer'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util'; import { InnerJoin } from '../classes/joins/inner_join'; @@ -55,7 +58,8 @@ import { ILayer } from '../classes/layers/layer'; export function createLayerInstance( layerDescriptor: LayerDescriptor, - inspectorAdapters?: Adapters + inspectorAdapters?: Adapters, + chartsPaletteServiceGetColor?: (value: string) => string | null ): ILayer { const source: ISource = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters); @@ -75,6 +79,7 @@ export function createLayerInstance( layerDescriptor: vectorLayerDescriptor, source: source as IVectorSource, joins, + chartsPaletteServiceGetColor, }); case VectorTileLayer.type: return new VectorTileLayer({ layerDescriptor, source: source as ITMSSource }); @@ -84,6 +89,7 @@ export function createLayerInstance( return new BlendedVectorLayer({ layerDescriptor: layerDescriptor as VectorLayerDescriptor, source: source as IVectorSource, + chartsPaletteServiceGetColor, }); case TiledVectorLayer.type: return new TiledVectorLayer({ @@ -295,9 +301,10 @@ export const getSpatialFiltersLayer = createSelector( export const getLayerList = createSelector( getLayerListRaw, getInspectorAdapters, - (layerDescriptorList, inspectorAdapters) => { + getChartsPaletteServiceGetColor, + (layerDescriptorList, inspectorAdapters, chartsPaletteServiceGetColor) => { return layerDescriptorList.map((layerDescriptor) => - createLayerInstance(layerDescriptor, inspectorAdapters) + createLayerInstance(layerDescriptor, inspectorAdapters, chartsPaletteServiceGetColor) ); } ); From 3d068c5db947799b958cef3bc190d3b102aca0b9 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 9 Feb 2021 15:03:23 +0000 Subject: [PATCH 058/100] [ML] Lazy ml node UI improvements (#90455) * [ML] Lazy ml node UI improvements * fixing test * adding awaitingMlNodeAllocation to default datafeed response * changing datafeed icon when node is not assigned * updating text Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/ml/common/types/modules.ts | 1 + .../jobs_awaiting_node_warning.tsx | 8 +++--- .../new_job_awaiting_node.tsx | 9 ++++-- .../new_job/recognize/components/job_item.tsx | 17 +++++++++-- .../jobs/new_job/recognize/page.tsx | 20 +++++++++++-- .../ml_nodes_check/check_ml_nodes.ts | 8 ++++++ .../application/ml_nodes_check/index.ts | 1 + .../forecasting_modal/run_controls.js | 4 +-- .../models/data_recognizer/data_recognizer.ts | 28 ++++++++++++++++--- .../apis/ml/modules/setup_module.ts | 1 + 10 files changed, 79 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/ml/common/types/modules.ts b/x-pack/plugins/ml/common/types/modules.ts index faa9c700f95a4..7c9623d3e68ec 100644 --- a/x-pack/plugins/ml/common/types/modules.ts +++ b/x-pack/plugins/ml/common/types/modules.ts @@ -68,6 +68,7 @@ export interface KibanaObjectResponse extends ResultItem { export interface DatafeedResponse extends ResultItem { started: boolean; + awaitingMlNodeAllocation?: boolean; error?: ErrorType; } diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx index bc216ce62a57c..2cc36b7a2adf7 100644 --- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx +++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx @@ -9,14 +9,14 @@ import React, { Fragment, FC } from 'react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { isCloud } from '../../services/ml_server_info'; +import { lazyMlNodesAvailable } from '../../ml_nodes_check'; interface Props { jobCount: number; } export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => { - if (isCloud() === false || jobCount === 0) { + if (lazyMlNodesAvailable() === false || jobCount === 0) { return null; } @@ -26,7 +26,7 @@ export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => { title={ } color="primary" @@ -35,7 +35,7 @@ export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => {
= () => { + if (lazyMlNodesAvailable() === false) { + return null; + } + return ( } color="primary" @@ -31,7 +36,7 @@ export const NewJobAwaitingNodeWarning: FC = () => {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx index 760ff67d97b9d..311e291cf2519 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx @@ -21,7 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ModuleJobUI } from '../page'; import { SETUP_RESULTS_WIDTH } from './module_jobs'; import { tabColor } from '../../../../../../common/util/group_color_utils'; -import { JobOverride } from '../../../../../../common/types/modules'; +import { JobOverride, DatafeedResponse } from '../../../../../../common/types/modules'; import { extractErrorMessage } from '../../../../../../common/util/errors'; interface JobItemProps { @@ -151,8 +151,8 @@ export const JobItem: FC = memo( = memo( ); } ); + +function getDatafeedStartedIcon({ + awaitingMlNodeAllocation, + success, +}: DatafeedResponse): { type: string; color: string } { + if (awaitingMlNodeAllocation === true) { + return { type: 'alert', color: 'warning' }; + } + + return success ? { type: 'check', color: 'secondary' } : { type: 'cross', color: 'danger' }; +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index 14018d485e04c..271898654ca49 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -43,6 +43,7 @@ import { TimeRange } from '../common/components'; import { JobId } from '../../../../../common/types/anomaly_detection_jobs'; import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; import { TIME_FORMAT } from '../../../../../common/constants/time_format'; +import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning'; export interface ModuleJobUI extends ModuleJob { datafeedResult?: DatafeedResponse; @@ -84,6 +85,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { const [saveState, setSaveState] = useState(SAVE_STATE.NOT_SAVED); const [resultsUrl, setResultsUrl] = useState(''); const [existingGroups, setExistingGroups] = useState(existingGroupIds); + const [jobsAwaitingNodeCount, setJobsAwaitingNodeCount] = useState(0); // #endregion const { @@ -204,9 +206,19 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { }); setResultsUrl(url); - const failedJobsCount = jobsResponse.reduce((count, { success }) => { - return success ? count : count + 1; - }, 0); + const failedJobsCount = jobsResponse.reduce( + (count, { success }) => (success ? count : count + 1), + 0 + ); + + const lazyJobsCount = datafeedsResponse.reduce( + (count, { awaitingMlNodeAllocation }) => + awaitingMlNodeAllocation === true ? count + 1 : count, + 0 + ); + + setJobsAwaitingNodeCount(lazyJobsCount); + setSaveState( failedJobsCount === 0 ? SAVE_STATE.SAVED @@ -291,6 +303,8 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { )} + {jobsAwaitingNodeCount > 0 && } + diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts index 71aef2da312a6..551a5823c1f41 100644 --- a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts +++ b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts @@ -48,6 +48,14 @@ export function mlNodesAvailable() { return mlNodeCount !== 0 || lazyMlNodeCount !== 0; } +export function currentMlNodesAvailable() { + return mlNodeCount !== 0; +} + +export function lazyMlNodesAvailable() { + return lazyMlNodeCount !== 0; +} + export function permissionToViewMlNodeCount() { return userHasPermissionToViewMlNodeCount; } diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts index 295ff1aca2ec7..8102f95c035b0 100644 --- a/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts +++ b/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts @@ -9,5 +9,6 @@ export { checkMlNodesAvailable, getMlNodeCount, mlNodesAvailable, + lazyMlNodesAvailable, permissionToViewMlNodeCount, } from './check_ml_nodes'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js index a37ad5fd30517..b36acba8b4ba4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -27,7 +27,7 @@ import { import { JOB_STATE } from '../../../../../common/constants/states'; import { FORECAST_DURATION_MAX_DAYS } from './forecasting_modal'; import { ForecastProgress } from './forecast_progress'; -import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; +import { currentMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { checkPermission, createPermissionFailureMessage, @@ -41,7 +41,7 @@ function getRunInputDisabledState(job, isForecastRequested) { // - No canForecastJob permission // - Job is not in an OPENED or CLOSED state // - A new forecast has been requested - if (mlNodesAvailable() === false) { + if (currentMlNodesAvailable() === false) { return { isDisabled: true, isDisabledToolTipText: i18n.translate( diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 92dfe3aa0fbf9..a1fac92d45b4e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -491,6 +491,7 @@ export class DataRecognizer { const startedDatafeed = startResults[df.id]; if (startedDatafeed !== undefined) { df.started = startedDatafeed.started; + df.awaitingMlNodeAllocation = startedDatafeed.awaitingMlNodeAllocation; if (startedDatafeed.error !== undefined) { df.error = startedDatafeed.error; } @@ -749,9 +750,20 @@ export class DataRecognizer { datafeeds.map(async (datafeed) => { try { await this.saveDatafeed(datafeed); - return { id: datafeed.id, success: true, started: false }; + return { + id: datafeed.id, + success: true, + started: false, + awaitingMlNodeAllocation: false, + }; } catch ({ body }) { - return { id: datafeed.id, success: false, started: false, error: body }; + return { + id: datafeed.id, + success: false, + started: false, + awaitingMlNodeAllocation: false, + error: body, + }; } }) ); @@ -811,11 +823,18 @@ export class DataRecognizer { duration.end = (end as unknown) as string; } - await this._mlClient.startDatafeed({ + const { + body: { started, node }, + } = await this._mlClient.startDatafeed<{ + started: boolean; + node: string; + }>({ datafeed_id: datafeed.id, ...duration, }); - result.started = true; + + result.started = started; + result.awaitingMlNodeAllocation = node?.length === 0; } catch ({ body }) { result.started = false; result.error = body; @@ -845,6 +864,7 @@ export class DataRecognizer { if (d.id === d2.id) { d.success = d2.success; d.started = d2.started; + d.awaitingMlNodeAllocation = d2.awaitingMlNodeAllocation; if (d2.error !== undefined) { d.error = d2.error; } diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index be1ac7fbb0965..8064d498774a3 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -772,6 +772,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRspDatafeeds = sortBy( testData.expected.jobs.map((job) => { return { + awaitingMlNodeAllocation: false, id: `datafeed-${job.jobId}`, success: true, started: testData.requestBody.startDatafeed, From 32ddd5e795b20d4b6d452412925d1921613d7336 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 9 Feb 2021 15:28:59 +0000 Subject: [PATCH 059/100] [Logs UI] Show anomalies across both the log rate and categorization ML jobs in a swimlane visualization. (#89589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow anomalies visualisation to represent anomalies from all jobs Co-authored-by: Felix Stürmer --- .../http_api/log_analysis/results/index.ts | 1 - .../log_analysis/results/log_entry_rate.ts | 86 --------- .../log_analysis/log_analysis_results.ts | 4 - x-pack/plugins/infra/kibana.json | 12 +- .../anomaly_severity_indicator.tsx | 9 +- .../missing_embeddable_factory_callout.tsx | 26 +++ .../hooks/use_kibana_timefilter_time.tsx | 39 +++- .../log_entry_rate/page_results_content.tsx | 173 +++++------------ .../anomalies_swimlane_visualisation.tsx | 70 +++++++ .../sections/anomalies/chart.tsx | 181 ----------------- .../sections/anomalies/index.tsx | 168 +++++----------- .../sections/anomalies/table.tsx | 10 +- .../sections/helpers/data_formatters.tsx | 182 ------------------ .../service_calls/get_log_entry_rate.ts | 43 ----- .../log_entry_rate/use_dataset_filtering.ts | 99 ++++++++++ .../use_log_entry_rate_results.ts | 160 --------------- .../use_log_entry_rate_results_url_state.tsx | 125 +++++++++--- x-pack/plugins/infra/public/types.ts | 2 + .../infra/public/utils/use_url_state.ts | 16 +- x-pack/plugins/infra/server/infra_server.ts | 2 - .../lib/log_analysis/log_entry_anomalies.ts | 1 - .../routes/log_analysis/results/index.ts | 1 - .../log_analysis/results/log_entry_rate.ts | 90 --------- .../swimlane_input_resolver.ts | 2 +- x-pack/plugins/ml/public/index.ts | 12 +- .../apply_influencer_filters_action.tsx | 3 +- .../plugins/ml/public/ui_actions/constants.ts | 8 + .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../api_integration/apis/metrics_ui/index.js | 1 - .../apis/metrics_ui/log_analysis.ts | 137 ------------- 31 files changed, 469 insertions(+), 1202 deletions(-) delete mode 100644 x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts create mode 100644 x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts create mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts delete mode 100644 x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts create mode 100644 x-pack/plugins/ml/public/ui_actions/constants.ts delete mode 100644 x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts index d50495689e9d8..23c2ce5f0c21f 100644 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts +++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts @@ -9,7 +9,6 @@ export * from './log_entry_categories'; export * from './log_entry_category_datasets'; export * from './log_entry_category_datasets_stats'; export * from './log_entry_category_examples'; -export * from './log_entry_rate'; export * from './log_entry_examples'; export * from './log_entry_anomalies'; export * from './log_entry_anomalies_datasets'; diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts deleted file mode 100644 index 943e1df70c0ba..0000000000000 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts +++ /dev/null @@ -1,86 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; - -import { badRequestErrorRT, conflictErrorRT, forbiddenErrorRT, timeRangeRT } from '../../shared'; - -export const LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH = - '/api/infra/log_analysis/results/log_entry_rate'; - -/** - * request - */ - -export const getLogEntryRateRequestPayloadRT = rt.type({ - data: rt.intersection([ - rt.type({ - bucketDuration: rt.number, - sourceId: rt.string, - timeRange: timeRangeRT, - }), - rt.partial({ - datasets: rt.array(rt.string), - }), - ]), -}); - -export type GetLogEntryRateRequestPayload = rt.TypeOf; - -/** - * response - */ - -export const logEntryRateAnomalyRT = rt.type({ - id: rt.string, - actualLogEntryRate: rt.number, - anomalyScore: rt.number, - duration: rt.number, - startTime: rt.number, - typicalLogEntryRate: rt.number, -}); - -export type LogEntryRateAnomaly = rt.TypeOf; - -export const logEntryRatePartitionRT = rt.type({ - analysisBucketCount: rt.number, - anomalies: rt.array(logEntryRateAnomalyRT), - averageActualLogEntryRate: rt.number, - maximumAnomalyScore: rt.number, - numberOfLogEntries: rt.number, - partitionId: rt.string, -}); - -export type LogEntryRatePartition = rt.TypeOf; - -export const logEntryRateHistogramBucketRT = rt.type({ - partitions: rt.array(logEntryRatePartitionRT), - startTime: rt.number, -}); - -export type LogEntryRateHistogramBucket = rt.TypeOf; - -export const getLogEntryRateSuccessReponsePayloadRT = rt.type({ - data: rt.type({ - bucketDuration: rt.number, - histogramBuckets: rt.array(logEntryRateHistogramBucketRT), - totalNumberOfLogEntries: rt.number, - }), -}); - -export type GetLogEntryRateSuccessResponsePayload = rt.TypeOf< - typeof getLogEntryRateSuccessReponsePayloadRT ->; - -export const getLogEntryRateResponsePayloadRT = rt.union([ - getLogEntryRateSuccessReponsePayloadRT, - badRequestErrorRT, - conflictErrorRT, - forbiddenErrorRT, -]); - -export type GetLogEntryRateReponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts b/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts index f460747f8b142..113e8ff8c34e6 100644 --- a/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts +++ b/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts @@ -40,10 +40,6 @@ export const getSeverityCategoryForScore = ( } }; -export const formatAnomalyScore = (score: number) => { - return Math.round(score); -}; - export const formatOneDecimalPlace = (number: number) => { return Math.round(number * 10) / 10; }; diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index 327cb674de00b..c892f7017da33 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -13,9 +13,17 @@ "alerts", "triggersActionsUi" ], - "optionalPlugins": ["ml", "observability", "home"], + "optionalPlugins": ["ml", "observability", "home", "embeddable"], "server": true, "ui": true, "configPath": ["xpack", "infra"], - "requiredBundles": ["observability", "licenseManagement", "kibanaUtils", "kibanaReact", "home"] + "requiredBundles": [ + "observability", + "licenseManagement", + "kibanaUtils", + "kibanaReact", + "home", + "ml", + "embeddable" + ] } diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx index 6fcbb0f6ffd4c..20fe816d1dab2 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx @@ -7,18 +7,15 @@ import { EuiHealth } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { - formatAnomalyScore, - getSeverityCategoryForScore, - ML_SEVERITY_COLORS, -} from '../../../../common/log_analysis'; +import { getFormattedSeverityScore } from '../../../../../ml/public'; +import { getSeverityCategoryForScore, ML_SEVERITY_COLORS } from '../../../../common/log_analysis'; export const AnomalySeverityIndicator: React.FunctionComponent<{ anomalyScore: number; }> = ({ anomalyScore }) => { const severityColor = useMemo(() => getColorForAnomalyScore(anomalyScore), [anomalyScore]); - return {formatAnomalyScore(anomalyScore)}; + return {getFormattedSeverityScore(anomalyScore)}; }; const getColorForAnomalyScore = (anomalyScore: number) => { diff --git a/x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx b/x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx new file mode 100644 index 0000000000000..8afd8cde32ef3 --- /dev/null +++ b/x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const MissingEmbeddableFactoryCallout: React.FC<{ embeddableType: string }> = ({ + embeddableType, +}) => { + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx b/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx index 12b0cb06d8682..15eb525dca734 100644 --- a/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx +++ b/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import useMount from 'react-use/lib/useMount'; - import { useKibanaContextForPlugin } from './use_kibana'; import { TimeRange, TimefilterContract } from '../../../../../src/plugins/data/public'; @@ -29,8 +28,24 @@ export const useKibanaTimefilterTime = ({ return [getTime, services.data.query.timefilter.timefilter.setTime]; }; -export const useSyncKibanaTimeFilterTime = (defaults: TimeRange, currentTimeRange: TimeRange) => { - const [, setTime] = useKibanaTimefilterTime(defaults); +/** + * Handles one or two way syncing with the Kibana time filter service. + * + * For one way syncing the time range will be synced back to the time filter service + * on mount *if* it differs from the defaults, e.g. a URL param. + * Future updates, after mount, will also be synced back to the time filter service. + * + * For two way syncing, in addition to the above, changes *from* the time filter service + * will be sycned to the solution, e.g. there might be an embeddable on the page that + * fires an action that hooks into the time filter service. + */ +export const useSyncKibanaTimeFilterTime = ( + defaults: TimeRange, + currentTimeRange: TimeRange, + setTimeRange?: (timeRange: TimeRange) => void +) => { + const { services } = useKibanaContextForPlugin(); + const [getTime, setTime] = useKibanaTimefilterTime(defaults); // On first mount we only want to sync time with Kibana if the derived currentTimeRange (e.g. from URL params) // differs from our defaults. @@ -40,8 +55,22 @@ export const useSyncKibanaTimeFilterTime = (defaults: TimeRange, currentTimeRang } }); - // Sync explicit changes *after* mount back to Kibana + // Sync explicit changes *after* mount from the solution back to Kibana useUpdateEffect(() => { setTime({ from: currentTimeRange.from, to: currentTimeRange.to }); }, [currentTimeRange.from, currentTimeRange.to, setTime]); + + // *Optionally* sync time filter service changes back to the solution. + // For example, an embeddable might have a time range action that hooks into + // the time filter service. + useEffect(() => { + const sub = services.data.query.timefilter.timefilter.getTimeUpdate$().subscribe(() => { + if (setTimeRange) { + const timeRange = getTime(); + setTimeRange(timeRange); + } + }); + + return () => sub.unsubscribe(); + }, [getTime, setTimeRange, services.data.query.timefilter.timefilter]); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index a8660e1ce8013..54617d025652b 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -5,17 +5,14 @@ * 2.0. */ -import datemath from '@elastic/datemath'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui'; import moment from 'moment'; import { stringify } from 'query-string'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { encode, RisonValue } from 'rison-node'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../../../../../observability/public'; -import { TimeRange } from '../../../../common/time/time_range'; -import { bucketSpan } from '../../../../common/log_analysis'; import { TimeKey } from '../../../../common/time'; import { CategoryJobNoticesSection, @@ -29,14 +26,11 @@ import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; import { useLogEntryFlyoutContext } from '../../../containers/logs/log_flyout'; import { useLogSourceContext } from '../../../containers/logs/log_source'; -import { useInterval } from '../../../hooks/use_interval'; import { AnomaliesResults } from './sections/anomalies'; import { useLogEntryAnomaliesResults } from './use_log_entry_anomalies_results'; -import { useLogEntryRateResults } from './use_log_entry_rate_results'; -import { - StringTimeRange, - useLogAnalysisResultsUrlState, -} from './use_log_entry_rate_results_url_state'; +import { useDatasetFiltering } from './use_dataset_filtering'; +import { useLogAnalysisResultsUrlState } from './use_log_entry_rate_results_url_state'; +import { isJobStatusWithResults } from '../../../../common/log_analysis'; export const SORT_DEFAULTS = { direction: 'desc' as const, @@ -62,6 +56,8 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { hasStoppedJobs: hasStoppedLogEntryRateJobs, moduleDescriptor: logEntryRateModuleDescriptor, setupStatus: logEntryRateSetupStatus, + jobStatus: logEntryRateJobStatus, + jobIds: logEntryRateJobIds, } = useLogEntryRateModuleContext(); const { @@ -71,10 +67,29 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { hasStoppedJobs: hasStoppedLogEntryCategoriesJobs, moduleDescriptor: logEntryCategoriesModuleDescriptor, setupStatus: logEntryCategoriesSetupStatus, + jobStatus: logEntryCategoriesJobStatus, + jobIds: logEntryCategoriesJobIds, } = useLogEntryCategoriesModuleContext(); + const jobIds = useMemo(() => { + return [ + ...(isJobStatusWithResults(logEntryRateJobStatus['log-entry-rate']) + ? [logEntryRateJobIds['log-entry-rate']] + : []), + ...(isJobStatusWithResults(logEntryCategoriesJobStatus['log-entry-categories-count']) + ? [logEntryCategoriesJobIds['log-entry-categories-count']] + : []), + ]; + }, [ + logEntryRateJobIds, + logEntryCategoriesJobIds, + logEntryRateJobStatus, + logEntryCategoriesJobStatus, + ]); + const { - timeRange: selectedTimeRange, + timeRange, + friendlyTimeRange, setTimeRange: setSelectedTimeRange, autoRefresh, setAutoRefresh, @@ -86,21 +101,13 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { logEntryId: flyoutLogEntryId, } = useLogEntryFlyoutContext(); - const [queryTimeRange, setQueryTimeRange] = useState<{ - value: TimeRange; - lastChangedTime: number; - }>(() => ({ - value: stringToNumericTimeRange(selectedTimeRange), - lastChangedTime: Date.now(), - })); - const linkToLogStream = useCallback( (filter: string, id: string, timeKey?: TimeKey) => { const params = { logPosition: encode({ - end: moment(queryTimeRange.value.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + end: moment(timeRange.value.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), position: timeKey as RisonValue, - start: moment(queryTimeRange.value.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + start: moment(timeRange.value.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), streamLive: false, }), flyoutOptions: encode({ @@ -114,23 +121,10 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { navigateToApp?.('logs', { path: `/stream?${stringify(params)}` }); }, - [queryTimeRange, navigateToApp] - ); - - const bucketDuration = useMemo( - () => getBucketDuration(queryTimeRange.value.startTime, queryTimeRange.value.endTime), - [queryTimeRange.value.endTime, queryTimeRange.value.startTime] + [timeRange, navigateToApp] ); - const [selectedDatasets, setSelectedDatasets] = useState([]); - - const { getLogEntryRate, isLoading, logEntryRate } = useLogEntryRateResults({ - sourceId, - startTime: queryTimeRange.value.startTime, - endTime: queryTimeRange.value.endTime, - bucketDuration, - filteredDatasets: selectedDatasets, - }); + const { selectedDatasets, setSelectedDatasets } = useDatasetFiltering(); const { isLoadingLogEntryAnomalies, @@ -146,48 +140,13 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { isLoadingDatasets, } = useLogEntryAnomaliesResults({ sourceId, - startTime: queryTimeRange.value.startTime, - endTime: queryTimeRange.value.endTime, + startTime: timeRange.value.startTime, + endTime: timeRange.value.endTime, defaultSortOptions: SORT_DEFAULTS, defaultPaginationOptions: PAGINATION_DEFAULTS, filteredDatasets: selectedDatasets, }); - const handleQueryTimeRangeChange = useCallback( - ({ start: startTime, end: endTime }: { start: string; end: string }) => { - setQueryTimeRange({ - value: stringToNumericTimeRange({ startTime, endTime }), - lastChangedTime: Date.now(), - }); - }, - [setQueryTimeRange] - ); - - const handleSelectedTimeRangeChange = useCallback( - (selectedTime: { start: string; end: string; isInvalid: boolean }) => { - if (selectedTime.isInvalid) { - return; - } - setSelectedTimeRange({ - startTime: selectedTime.start, - endTime: selectedTime.end, - }); - handleQueryTimeRangeChange(selectedTime); - }, - [setSelectedTimeRange, handleQueryTimeRangeChange] - ); - - const handleChartTimeRangeChange = useCallback( - ({ startTime, endTime }: TimeRange) => { - handleSelectedTimeRangeChange({ - end: new Date(endTime).toISOString(), - isInvalid: false, - start: new Date(startTime).toISOString(), - }); - }, - [handleSelectedTimeRangeChange] - ); - const handleAutoRefreshChange = useCallback( ({ isPaused, refreshInterval: interval }: { isPaused: boolean; refreshInterval: number }) => { setAutoRefresh({ @@ -207,7 +166,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { showModuleSetup, ]); - const hasLogRateResults = (logEntryRate?.histogramBuckets?.length ?? 0) > 0; const hasAnomalyResults = logEntryAnomalies.length > 0; const isFirstUse = useMemo( @@ -217,22 +175,18 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { logEntryCategoriesSetupStatus.type === 'succeeded' || (logEntryRateSetupStatus.type === 'skipped' && !!logEntryRateSetupStatus.newlyCreated) || logEntryRateSetupStatus.type === 'succeeded') && - !(hasLogRateResults || hasAnomalyResults), - [hasAnomalyResults, hasLogRateResults, logEntryCategoriesSetupStatus, logEntryRateSetupStatus] + !hasAnomalyResults, + [hasAnomalyResults, logEntryCategoriesSetupStatus, logEntryRateSetupStatus] ); - useEffect(() => { - getLogEntryRate(); - }, [getLogEntryRate, selectedDatasets, queryTimeRange.lastChangedTime]); - - useInterval( - () => { - handleQueryTimeRangeChange({ - start: selectedTimeRange.startTime, - end: selectedTimeRange.endTime, - }); + const handleSelectedTimeRangeChange = useCallback( + (selectedTime: { start: string; end: string; isInvalid: boolean }) => { + if (selectedTime.isInvalid) { + return; + } + setSelectedTimeRange(selectedTime); }, - autoRefresh.isPaused ? null : autoRefresh.interval + [setSelectedTimeRange] ); return ( @@ -251,8 +205,8 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { { { changePaginationOptions={changePaginationOptions} sortOptions={sortOptions} paginationOptions={paginationOptions} + selectedDatasets={selectedDatasets} + jobIds={jobIds} + autoRefresh={autoRefresh} /> @@ -318,37 +272,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { ); }; -const stringToNumericTimeRange = (timeRange: StringTimeRange): TimeRange => ({ - startTime: moment( - datemath.parse(timeRange.startTime, { - momentInstance: moment, - }) - ).valueOf(), - endTime: moment( - datemath.parse(timeRange.endTime, { - momentInstance: moment, - roundUp: true, - }) - ).valueOf(), -}); - -/** - * This function takes the current time range in ms, - * works out the bucket interval we'd need to always - * display 100 data points, and then takes that new - * value and works out the nearest multiple of - * 900000 (15 minutes) to it, so that we don't end up with - * jaggy bucket boundaries between the ML buckets and our - * aggregation buckets. - */ -const getBucketDuration = (startTime: number, endTime: number) => { - const msRange = moment(endTime).diff(moment(startTime)); - const bucketIntervalInMs = msRange / 100; - const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan); - const roundedResult = parseInt(Number(result).toFixed(0), 10); - return roundedResult < bucketSpan ? bucketSpan : roundedResult; -}; - // This is needed due to the flex-basis: 100% !important; rule that // kicks in on small screens via media queries breaking when using direction="column" export const ResultsContentPage = euiStyled(EuiPage)` diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx new file mode 100644 index 0000000000000..b0e85a4648d6e --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import moment from 'moment'; +import { AutoRefresh } from '../../use_log_entry_rate_results_url_state'; +import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; +import { + ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, + AnomalySwimlaneEmbeddableInput, +} from '../../../../../../../ml/public'; +import { EmbeddableRenderer } from '../../../../../../../../../src/plugins/embeddable/public'; +import { partitionField } from '../../../../../../common/infra_ml'; +import { MissingEmbeddableFactoryCallout } from '../../../../../components/missing_embeddable_factory_callout'; +import { TimeRange } from '../../../../../../common/time/time_range'; + +interface Props { + timeRange: TimeRange; + jobIds: string[]; + selectedDatasets: string[]; + autoRefresh: AutoRefresh; +} + +// Disable refresh, allow our timerange changes to refresh the embeddable. +const REFRESH_CONFIG = { + pause: true, + value: 0, +}; + +export const AnomaliesSwimlaneVisualisation: React.FC = (props) => { + const { embeddable: embeddablePlugin } = useKibanaContextForPlugin().services; + if (!embeddablePlugin) return null; + return ; +}; + +export const VisualisationContent: React.FC = ({ timeRange, jobIds, selectedDatasets }) => { + const { embeddable: embeddablePlugin } = useKibanaContextForPlugin().services; + const factory = embeddablePlugin?.getEmbeddableFactory(ANOMALY_SWIMLANE_EMBEDDABLE_TYPE); + + const embeddableInput: AnomalySwimlaneEmbeddableInput = useMemo(() => { + return { + id: 'LOG_ENTRY_ANOMALIES_EMBEDDABLE_INSTANCE', // NOTE: This is the only embeddable on the anomalies page, a static string will do. + jobIds, + swimlaneType: 'viewBy', + timeRange: { + from: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + to: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + }, + refreshConfig: REFRESH_CONFIG, + viewBy: partitionField, + filters: [], + query: { + language: 'kuery', + query: selectedDatasets + .map((dataset) => `${partitionField} : ${dataset !== '' ? dataset : '""'}`) + .join(' or '), // Ensure unknown (those with an empty "" string) datasets are handled correctly. + }, + }; + }, [jobIds, timeRange.startTime, timeRange.endTime, selectedDatasets]); + + if (!factory) { + return ; + } + + return ; +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx deleted file mode 100644 index dd9c2dd707044..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx +++ /dev/null @@ -1,181 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiEmptyPrompt } from '@elastic/eui'; -import { RectAnnotationDatum, AnnotationId } from '@elastic/charts'; -import { - Axis, - BarSeries, - Chart, - niceTimeFormatter, - Settings, - TooltipValue, - LIGHT_THEME, - DARK_THEME, - RectAnnotation, - BrushEndListener, -} from '@elastic/charts'; -import numeral from '@elastic/numeral'; -import { i18n } from '@kbn/i18n'; -import moment from 'moment'; -import React, { useCallback, useMemo } from 'react'; -import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; - -import { TimeRange } from '../../../../../../common/time/time_range'; -import { - MLSeverityScoreCategories, - ML_SEVERITY_COLORS, -} from '../../../../../../common/log_analysis'; -import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting'; - -export const AnomaliesChart: React.FunctionComponent<{ - chartId: string; - setTimeRange: (timeRange: TimeRange) => void; - timeRange: TimeRange; - series: Array<{ time: number; value: number }>; - annotations: Record; - renderAnnotationTooltip?: (details?: string) => JSX.Element; - isLoading: boolean; -}> = ({ - chartId, - series, - annotations, - setTimeRange, - timeRange, - renderAnnotationTooltip, - isLoading, -}) => { - const [dateFormat] = useKibanaUiSetting('dateFormat', 'Y-MM-DD HH:mm:ss.SSS'); - const [isDarkMode] = useKibanaUiSetting('theme:darkMode'); - - const chartDateFormatter = useMemo( - () => niceTimeFormatter([timeRange.startTime, timeRange.endTime]), - [timeRange] - ); - - const logEntryRateSpecId = 'averageValues'; - - const tooltipProps = useMemo( - () => ({ - headerFormatter: (tooltipData: TooltipValue) => moment(tooltipData.value).format(dateFormat), - }), - [dateFormat] - ); - - const handleBrushEnd = useCallback( - ({ x }) => { - if (!x) { - return; - } - const [startTime, endTime] = x; - setTimeRange({ - endTime, - startTime, - }); - }, - [setTimeRange] - ); - - return !isLoading && !series.length ? ( - - {i18n.translate('xpack.infra.logs.analysis.anomalySectionLogRateChartNoData', { - defaultMessage: 'There is no log rate data to display.', - })} - - } - titleSize="m" - /> - ) : ( - -
- {series.length ? ( - - - numeral(value.toPrecision(3)).format('0[.][00]a')} // https://github.com/adamwdraper/Numeral-js/issues/194 - /> - - {renderAnnotations(annotations, chartId, renderAnnotationTooltip)} - - - ) : null} -
-
- ); -}; - -interface SeverityConfig { - id: AnnotationId; - style: { - fill: string; - opacity: number; - }; -} - -const severityConfigs: Record = { - warning: { - id: `anomalies-warning`, - style: { fill: ML_SEVERITY_COLORS.warning, opacity: 0.7 }, - }, - minor: { - id: `anomalies-minor`, - style: { fill: ML_SEVERITY_COLORS.minor, opacity: 0.7 }, - }, - major: { - id: `anomalies-major`, - style: { fill: ML_SEVERITY_COLORS.major, opacity: 0.7 }, - }, - critical: { - id: `anomalies-critical`, - style: { fill: ML_SEVERITY_COLORS.critical, opacity: 0.7 }, - }, -}; - -const renderAnnotations = ( - annotations: Record, - chartId: string, - renderAnnotationTooltip?: (details?: string) => JSX.Element -) => { - return Object.entries(annotations).map((entry, index) => { - return ( - - ); - }); -}; - -const barSeriesStyle = { rect: { fill: '#D3DAE6', opacity: 0.6 } }; // TODO: Acquire this from "theme" as euiColorLightShade diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx index 75d7c4212bbc3..3bc206e9ad7bb 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx @@ -14,12 +14,9 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { LogEntryRateResults } from '../../use_log_entry_rate_results'; +import React from 'react'; import { TimeRange } from '../../../../../../common/time/time_range'; -import { getAnnotationsForAll, getLogEntryRateCombinedSeries } from '../helpers/data_formatters'; -import { AnomaliesChart } from './chart'; +import { AnomaliesSwimlaneVisualisation } from './anomalies_swimlane_visualisation'; import { AnomaliesTable } from './table'; import { ManageJobsButton } from '../../../../../components/logging/log_analysis_setup/manage_jobs_button'; import { @@ -33,13 +30,11 @@ import { SortOptions, } from '../../use_log_entry_anomalies_results'; import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; +import { AutoRefresh } from '../../use_log_entry_rate_results_url_state'; export const AnomaliesResults: React.FunctionComponent<{ - isLoadingLogRateResults: boolean; isLoadingAnomaliesResults: boolean; - logEntryRateResults: LogEntryRateResults | null; anomalies: LogEntryAnomalies; - setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; onViewModuleList: () => void; page: Page; @@ -49,11 +44,11 @@ export const AnomaliesResults: React.FunctionComponent<{ changePaginationOptions: ChangePaginationOptions; sortOptions: SortOptions; paginationOptions: PaginationOptions; + selectedDatasets: string[]; + jobIds: string[]; + autoRefresh: AutoRefresh; }> = ({ - isLoadingLogRateResults, isLoadingAnomaliesResults, - logEntryRateResults, - setTimeRange, timeRange, onViewModuleList, anomalies, @@ -64,27 +59,10 @@ export const AnomaliesResults: React.FunctionComponent<{ fetchNextPage, fetchPreviousPage, page, + selectedDatasets, + jobIds, + autoRefresh, }) => { - const logEntryRateSeries = useMemo( - () => - logEntryRateResults && logEntryRateResults.histogramBuckets - ? getLogEntryRateCombinedSeries(logEntryRateResults) - : [], - [logEntryRateResults] - ); - const anomalyAnnotations = useMemo( - () => - logEntryRateResults && logEntryRateResults.histogramBuckets - ? getAnnotationsForAll(logEntryRateResults) - : { - warning: [], - minor: [], - major: [], - critical: [], - }, - [logEntryRateResults] - ); - return ( <> @@ -98,52 +76,44 @@ export const AnomaliesResults: React.FunctionComponent<{
- {(!logEntryRateResults || - (logEntryRateResults && - logEntryRateResults.histogramBuckets && - !logEntryRateResults.histogramBuckets.length)) && - (!anomalies || anomalies.length === 0) ? ( - } - > - - {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataTitle', { - defaultMessage: 'There is no data to display.', - })} - - } - titleSize="m" - body={ -

- {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataBody', { - defaultMessage: 'You may want to adjust your time range.', - })} -

- } + + + -
- ) : ( - <> - - - - - - +
+ + + <> + {!anomalies || anomalies.length === 0 ? ( + } + > + + {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataTitle', { + defaultMessage: 'There is no data to display.', + })} + + } + titleSize="m" + body={ +

+ {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataBody', { + defaultMessage: 'You may want to adjust your time range.', + })} +

+ } + /> +
+ ) : ( - - )} + )} + ); }; @@ -164,52 +134,6 @@ const title = i18n.translate('xpack.infra.logs.analysis.anomaliesSectionTitle', defaultMessage: 'Anomalies', }); -interface ParsedAnnotationDetails { - anomalyScoresByPartition: Array<{ partitionName: string; maximumAnomalyScore: number }>; -} - -const overallAnomalyScoreLabel = i18n.translate( - 'xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel', - { - defaultMessage: 'Max anomaly scores:', - } -); - -const AnnotationTooltip: React.FunctionComponent<{ details: string }> = ({ details }) => { - const parsedDetails: ParsedAnnotationDetails = JSON.parse(details); - return ( - - - {overallAnomalyScoreLabel} - -
    - {parsedDetails.anomalyScoresByPartition.map(({ partitionName, maximumAnomalyScore }) => { - return ( -
  • - - {`${partitionName}: `} - {maximumAnomalyScore} - -
  • - ); - })} -
-
- ); -}; - -const renderAnnotationTooltip = (details?: string) => { - // Note: Seems to be necessary to get things typed correctly all the way through to elastic-charts components - if (!details) { - return
; - } - return ; -}; - -const TooltipWrapper = euiStyled('div')` - white-space: nowrap; -`; - const loadingAriaLabel = i18n.translate( 'xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel', { defaultMessage: 'Loading anomalies' } diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index d80f9d04e72a8..c208c72558362 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -22,7 +22,6 @@ import useSet from 'react-use/lib/useSet'; import { TimeRange } from '../../../../../../common/time/time_range'; import { AnomalyType, - formatAnomalyScore, getFriendlyNameForPartitionId, formatOneDecimalPlace, isCategoryAnomaly, @@ -47,7 +46,6 @@ import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay interface TableItem { id: string; dataset: string; - datasetName: string; anomalyScore: number; startTime: number; typical: number; @@ -86,7 +84,6 @@ const datasetColumnName = i18n.translate( export const AnomaliesTable: React.FunctionComponent<{ results: LogEntryAnomalies; - setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; changeSortOptions: ChangeSortOptions; changePaginationOptions: ChangePaginationOptions; @@ -99,7 +96,6 @@ export const AnomaliesTable: React.FunctionComponent<{ }> = ({ results, timeRange, - setTimeRange, changeSortOptions, sortOptions, changePaginationOptions, @@ -122,8 +118,7 @@ export const AnomaliesTable: React.FunctionComponent<{ return { id: anomaly.id, dataset: anomaly.dataset, - datasetName: getFriendlyNameForPartitionId(anomaly.dataset), - anomalyScore: formatAnomalyScore(anomaly.anomalyScore), + anomalyScore: anomaly.anomalyScore, startTime: anomaly.startTime, type: anomaly.type, typical: anomaly.typical, @@ -182,11 +177,12 @@ export const AnomaliesTable: React.FunctionComponent<{ render: (startTime: number) => moment(startTime).format(dateFormat), }, { - field: 'datasetName', + field: 'dataset', name: datasetColumnName, sortable: true, truncateText: true, width: '200px', + render: (dataset: string) => getFriendlyNameForPartitionId(dataset), }, { align: RIGHT_ALIGNMENT, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx deleted file mode 100644 index 8041ad1458546..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx +++ /dev/null @@ -1,182 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { RectAnnotationDatum } from '@elastic/charts'; -import { i18n } from '@kbn/i18n'; - -import { - formatAnomalyScore, - getFriendlyNameForPartitionId, - getSeverityCategoryForScore, - MLSeverityScoreCategories, -} from '../../../../../../common/log_analysis'; -import { LogEntryRateResults } from '../../use_log_entry_rate_results'; - -export const getLogEntryRatePartitionedSeries = (results: LogEntryRateResults) => { - return results.histogramBuckets.reduce>( - (buckets, bucket) => { - return [ - ...buckets, - ...bucket.partitions.map((partition) => ({ - group: getFriendlyNameForPartitionId(partition.partitionId), - time: bucket.startTime, - value: partition.averageActualLogEntryRate, - })), - ]; - }, - [] - ); -}; - -export const getLogEntryRateCombinedSeries = (results: LogEntryRateResults) => { - return results.histogramBuckets.reduce>( - (buckets, bucket) => { - return [ - ...buckets, - { - time: bucket.startTime, - value: bucket.partitions.reduce((accumulatedValue, partition) => { - return accumulatedValue + partition.averageActualLogEntryRate; - }, 0), - }, - ]; - }, - [] - ); -}; - -export const getLogEntryRateSeriesForPartition = ( - results: LogEntryRateResults, - partitionId: string -) => { - return results.partitionBuckets[partitionId].buckets.reduce< - Array<{ time: number; value: number }> - >((buckets, bucket) => { - return [ - ...buckets, - { - time: bucket.startTime, - value: bucket.averageActualLogEntryRate, - }, - ]; - }, []); -}; - -export const getAnnotationsForPartition = (results: LogEntryRateResults, partitionId: string) => { - return results.partitionBuckets[partitionId].buckets.reduce< - Record - >( - (annotatedBucketsBySeverity, bucket) => { - const severityCategory = getSeverityCategoryForScore(bucket.maximumAnomalyScore); - if (!severityCategory) { - return annotatedBucketsBySeverity; - } - - return { - ...annotatedBucketsBySeverity, - [severityCategory]: [ - ...annotatedBucketsBySeverity[severityCategory], - { - coordinates: { - x0: bucket.startTime, - x1: bucket.startTime + results.bucketDuration, - }, - details: i18n.translate( - 'xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel', - { - defaultMessage: 'Max anomaly score: {maxAnomalyScore}', - values: { - maxAnomalyScore: formatAnomalyScore(bucket.maximumAnomalyScore), - }, - } - ), - }, - ], - }; - }, - { - warning: [], - minor: [], - major: [], - critical: [], - } - ); -}; - -export const getTotalNumberOfLogEntriesForPartition = ( - results: LogEntryRateResults, - partitionId: string -) => { - return results.partitionBuckets[partitionId].totalNumberOfLogEntries; -}; - -export const getAnnotationsForAll = (results: LogEntryRateResults) => { - return results.histogramBuckets.reduce>( - (annotatedBucketsBySeverity, bucket) => { - const maxAnomalyScoresByPartition = bucket.partitions.reduce< - Array<{ partitionName: string; maximumAnomalyScore: number }> - >((bucketMaxAnomalyScoresByPartition, partition) => { - if (!getSeverityCategoryForScore(partition.maximumAnomalyScore)) { - return bucketMaxAnomalyScoresByPartition; - } - return [ - ...bucketMaxAnomalyScoresByPartition, - { - partitionName: getFriendlyNameForPartitionId(partition.partitionId), - maximumAnomalyScore: formatAnomalyScore(partition.maximumAnomalyScore), - }, - ]; - }, []); - - if (maxAnomalyScoresByPartition.length === 0) { - return annotatedBucketsBySeverity; - } - const severityCategory = getSeverityCategoryForScore( - Math.max( - ...maxAnomalyScoresByPartition.map((partitionScore) => partitionScore.maximumAnomalyScore) - ) - ); - if (!severityCategory) { - return annotatedBucketsBySeverity; - } - const sortedMaxAnomalyScoresByPartition = maxAnomalyScoresByPartition.sort( - (a, b) => b.maximumAnomalyScore - a.maximumAnomalyScore - ); - return { - ...annotatedBucketsBySeverity, - [severityCategory]: [ - ...annotatedBucketsBySeverity[severityCategory], - { - coordinates: { - x0: bucket.startTime, - x1: bucket.startTime + results.bucketDuration, - }, - details: JSON.stringify({ - anomalyScoresByPartition: sortedMaxAnomalyScoresByPartition, - }), - }, - ], - }; - }, - { - warning: [], - minor: [], - major: [], - critical: [], - } - ); -}; - -export const getTopAnomalyScoreAcrossAllPartitions = (results: LogEntryRateResults) => { - const allTopScores = Object.values(results.partitionBuckets).reduce( - (scores: number[], partition) => { - return [...scores, partition.topAnomalyScore]; - }, - [] - ); - return Math.max(...allTopScores); -}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts deleted file mode 100644 index 4b677140e2a7a..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts +++ /dev/null @@ -1,43 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { HttpHandler } from 'src/core/public'; -import { - getLogEntryRateRequestPayloadRT, - getLogEntryRateSuccessReponsePayloadRT, - LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, -} from '../../../../../common/http_api/log_analysis'; -import { decodeOrThrow } from '../../../../../common/runtime_types'; - -interface RequestArgs { - sourceId: string; - startTime: number; - endTime: number; - bucketDuration: number; - datasets?: string[]; -} - -export const callGetLogEntryRateAPI = async (requestArgs: RequestArgs, fetch: HttpHandler) => { - const { sourceId, startTime, endTime, bucketDuration, datasets } = requestArgs; - const response = await fetch(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, { - method: 'POST', - body: JSON.stringify( - getLogEntryRateRequestPayloadRT.encode({ - data: { - sourceId, - timeRange: { - startTime, - endTime, - }, - bucketDuration, - datasets, - }, - }) - ), - }); - return decodeOrThrow(getLogEntryRateSuccessReponsePayloadRT)(response); -}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts new file mode 100644 index 0000000000000..9bd1e42779a36 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useReducer, useCallback } from 'react'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { Filter } from '../../../../../../../src/plugins/data/common'; +import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../../../../ml/public'; + +interface ReducerState { + selectedDatasets: string[]; + selectedDatasetsFilters: Filter[]; +} + +type ReducerAction = + | { type: 'changeSelectedDatasets'; payload: { datasets: string[] } } + | { type: 'updateDatasetsFilters'; payload: { filters: Filter[] } }; + +const initialState: ReducerState = { + selectedDatasets: [], + selectedDatasetsFilters: [], +}; + +function reducer(state: ReducerState, action: ReducerAction) { + switch (action.type) { + case 'changeSelectedDatasets': + return { + ...state, + selectedDatasets: action.payload.datasets, + }; + case 'updateDatasetsFilters': + const datasetsToAdd = action.payload.filters + .filter((filter) => !state.selectedDatasets.includes(filter.meta.params.query)) + .map((filter) => filter.meta.params.query); + return { + ...state, + selectedDatasets: [...state.selectedDatasets, ...datasetsToAdd], + selectedDatasetsFilters: action.payload.filters, + }; + default: + throw new Error('Unknown action'); + } +} + +export const useDatasetFiltering = () => { + const { services } = useKibanaContextForPlugin(); + const [reducerState, dispatch] = useReducer(reducer, initialState); + + const handleSetSelectedDatasets = useCallback( + (datasets: string[]) => { + dispatch({ type: 'changeSelectedDatasets', payload: { datasets } }); + }, + [dispatch] + ); + + // NOTE: The anomaly swimlane embeddable will communicate it's filter action + // changes via the filterManager service. + useEffect(() => { + const sub = services.data.query.filterManager.getUpdates$().subscribe(() => { + const filters = services.data.query.filterManager + .getFilters() + .filter( + (filter) => + filter.meta.controlledBy && filter.meta.controlledBy === CONTROLLED_BY_SWIM_LANE_FILTER + ); + dispatch({ type: 'updateDatasetsFilters', payload: { filters } }); + }); + + return () => sub.unsubscribe(); + }, [services.data.query.filterManager, dispatch]); + + // NOTE: When filters are removed via the UI we need to make sure these are also tidied up + // within the FilterManager service, otherwise a scenario can occur where that filter can't + // be re-added via the embeddable as it will be seen as a duplicate to the FilterManager, + // and no update will be emitted. + useEffect(() => { + const filtersToRemove = reducerState.selectedDatasetsFilters.filter( + (filter) => !reducerState.selectedDatasets.includes(filter.meta.params.query) + ); + if (filtersToRemove.length > 0) { + filtersToRemove.forEach((filter) => { + services.data.query.filterManager.removeFilter(filter); + }); + } + }, [ + reducerState.selectedDatasets, + reducerState.selectedDatasetsFilters, + services.data.query.filterManager, + ]); + + return { + selectedDatasets: reducerState.selectedDatasets, + setSelectedDatasets: handleSetSelectedDatasets, + selectedDatasetsFilters: reducerState.selectedDatasetsFilters, + }; +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts deleted file mode 100644 index a226977a30c57..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts +++ /dev/null @@ -1,160 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useMemo, useState } from 'react'; - -import { - GetLogEntryRateSuccessResponsePayload, - LogEntryRateHistogramBucket, - LogEntryRatePartition, - LogEntryRateAnomaly, -} from '../../../../common/http_api/log_analysis'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callGetLogEntryRateAPI } from './service_calls/get_log_entry_rate'; - -type PartitionBucket = LogEntryRatePartition & { - startTime: number; -}; - -type PartitionRecord = Record< - string, - { buckets: PartitionBucket[]; topAnomalyScore: number; totalNumberOfLogEntries: number } ->; - -export type AnomalyRecord = LogEntryRateAnomaly & { - partitionId: string; -}; - -export interface LogEntryRateResults { - bucketDuration: number; - totalNumberOfLogEntries: number; - histogramBuckets: LogEntryRateHistogramBucket[]; - partitionBuckets: PartitionRecord; - anomalies: AnomalyRecord[]; -} - -export const useLogEntryRateResults = ({ - sourceId, - startTime, - endTime, - bucketDuration = 15 * 60 * 1000, - filteredDatasets, -}: { - sourceId: string; - startTime: number; - endTime: number; - bucketDuration: number; - filteredDatasets?: string[]; -}) => { - const { services } = useKibanaContextForPlugin(); - const [logEntryRate, setLogEntryRate] = useState(null); - - const [getLogEntryRateRequest, getLogEntryRate] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - return await callGetLogEntryRateAPI( - { - sourceId, - startTime, - endTime, - bucketDuration, - datasets: filteredDatasets, - }, - services.http.fetch - ); - }, - onResolve: ({ data }) => { - setLogEntryRate({ - bucketDuration: data.bucketDuration, - totalNumberOfLogEntries: data.totalNumberOfLogEntries, - histogramBuckets: data.histogramBuckets, - partitionBuckets: formatLogEntryRateResultsByPartition(data), - anomalies: formatLogEntryRateResultsByAllAnomalies(data), - }); - }, - onReject: () => { - setLogEntryRate(null); - }, - }, - [sourceId, startTime, endTime, bucketDuration, filteredDatasets] - ); - - const isLoading = useMemo(() => getLogEntryRateRequest.state === 'pending', [ - getLogEntryRateRequest.state, - ]); - - return { - getLogEntryRate, - isLoading, - logEntryRate, - }; -}; - -const formatLogEntryRateResultsByPartition = ( - results: GetLogEntryRateSuccessResponsePayload['data'] -): PartitionRecord => { - const partitionedBuckets = results.histogramBuckets.reduce< - Record - >((partitionResults, bucket) => { - return bucket.partitions.reduce>( - (_partitionResults, partition) => { - return { - ..._partitionResults, - [partition.partitionId]: { - buckets: _partitionResults[partition.partitionId] - ? [ - ..._partitionResults[partition.partitionId].buckets, - { startTime: bucket.startTime, ...partition }, - ] - : [{ startTime: bucket.startTime, ...partition }], - }, - }; - }, - partitionResults - ); - }, {}); - - const resultsByPartition: PartitionRecord = {}; - - Object.entries(partitionedBuckets).map(([key, value]) => { - const anomalyScores = value.buckets.reduce((scores: number[], bucket) => { - return [...scores, bucket.maximumAnomalyScore]; - }, []); - const totalNumberOfLogEntries = value.buckets.reduce((total, bucket) => { - return (total += bucket.numberOfLogEntries); - }, 0); - resultsByPartition[key] = { - topAnomalyScore: Math.max(...anomalyScores), - totalNumberOfLogEntries, - buckets: value.buckets, - }; - }); - - return resultsByPartition; -}; - -const formatLogEntryRateResultsByAllAnomalies = ( - results: GetLogEntryRateSuccessResponsePayload['data'] -): AnomalyRecord[] => { - return results.histogramBuckets.reduce((anomalies, bucket) => { - return bucket.partitions.reduce((_anomalies, partition) => { - if (partition.anomalies.length > 0) { - partition.anomalies.forEach((anomaly) => { - _anomalies.push({ - partitionId: partition.partitionId, - ...anomaly, - }); - }); - return _anomalies; - } else { - return _anomalies; - } - }, anomalies); - }, []); -}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx index fdbde1acb83ad..ccfae14fd4a59 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx @@ -5,24 +5,32 @@ * 2.0. */ -import { fold } from 'fp-ts/lib/Either'; -import { constant, identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; +import { useCallback, useMemo, useState } from 'react'; +import datemath from '@elastic/datemath'; +import moment from 'moment'; import * as rt from 'io-ts'; - +import { TimeRange as KibanaTimeRange } from '../../../../../../../src/plugins/data/public'; +import { TimeRange } from '../../../../common/time/time_range'; import { useUrlState } from '../../../utils/use_url_state'; +import { useInterval } from '../../../hooks/use_interval'; import { useKibanaTimefilterTime, useSyncKibanaTimeFilterTime, } from '../../../hooks/use_kibana_timefilter_time'; +import { decodeOrThrow } from '../../../../common/runtime_types'; -const autoRefreshRT = rt.union([ - rt.type({ - interval: rt.number, - isPaused: rt.boolean, - }), - rt.undefined, -]); +const autoRefreshRT = rt.type({ + interval: rt.number, + isPaused: rt.boolean, +}); + +export type AutoRefresh = rt.TypeOf; +const urlAutoRefreshRT = rt.union([autoRefreshRT, rt.undefined]); +const decodeAutoRefreshUrlState = decodeOrThrow(urlAutoRefreshRT); +const defaultAutoRefreshState = { + isPaused: false, + interval: 30000, +}; export const stringTimeRangeRT = rt.type({ startTime: rt.string, @@ -31,6 +39,7 @@ export const stringTimeRangeRT = rt.type({ export type StringTimeRange = rt.TypeOf; const urlTimeRangeRT = rt.union([stringTimeRangeRT, rt.undefined]); +const decodeTimeRangeUrlState = decodeOrThrow(urlTimeRangeRT); const TIME_RANGE_URL_STATE_KEY = 'timeRange'; const AUTOREFRESH_URL_STATE_KEY = 'autoRefresh'; @@ -40,36 +49,102 @@ export const useLogAnalysisResultsUrlState = () => { const [getTime] = useKibanaTimefilterTime(TIME_DEFAULTS); const { from: start, to: end } = getTime(); - const [timeRange, setTimeRange] = useUrlState({ - defaultState: { + const defaultTimeRangeState = useMemo(() => { + return { startTime: start, endTime: end, - }, - decodeUrlState: (value: unknown) => - pipe(urlTimeRangeRT.decode(value), fold(constant(undefined), identity)), + }; + }, [start, end]); + + const [urlTimeRange, setUrlTimeRange] = useUrlState({ + defaultState: defaultTimeRangeState, + decodeUrlState: decodeTimeRangeUrlState, encodeUrlState: urlTimeRangeRT.encode, urlStateKey: TIME_RANGE_URL_STATE_KEY, writeDefaultState: true, }); - useSyncKibanaTimeFilterTime(TIME_DEFAULTS, { from: timeRange.startTime, to: timeRange.endTime }); + // Numeric time range for querying APIs + const [queryTimeRange, setQueryTimeRange] = useState<{ + value: TimeRange; + lastChangedTime: number; + }>(() => ({ + value: stringToNumericTimeRange({ start: urlTimeRange.startTime, end: urlTimeRange.endTime }), + lastChangedTime: Date.now(), + })); - const [autoRefresh, setAutoRefresh] = useUrlState({ - defaultState: { - isPaused: false, - interval: 30000, + const handleQueryTimeRangeChange = useCallback( + ({ start: startTime, end: endTime }: { start: string; end: string }) => { + setQueryTimeRange({ + value: stringToNumericTimeRange({ start: startTime, end: endTime }), + lastChangedTime: Date.now(), + }); + }, + [setQueryTimeRange] + ); + + const setTimeRange = useCallback( + (selectedTime: { start: string; end: string }) => { + setUrlTimeRange({ + startTime: selectedTime.start, + endTime: selectedTime.end, + }); + handleQueryTimeRangeChange(selectedTime); }, - decodeUrlState: (value: unknown) => - pipe(autoRefreshRT.decode(value), fold(constant(undefined), identity)), - encodeUrlState: autoRefreshRT.encode, + [setUrlTimeRange, handleQueryTimeRangeChange] + ); + + const handleTimeFilterChange = useCallback( + (newTimeRange: KibanaTimeRange) => { + const { from, to } = newTimeRange; + setTimeRange({ start: from, end: to }); + }, + [setTimeRange] + ); + + useSyncKibanaTimeFilterTime( + TIME_DEFAULTS, + { from: urlTimeRange.startTime, to: urlTimeRange.endTime }, + handleTimeFilterChange + ); + + const [autoRefresh, setAutoRefresh] = useUrlState({ + defaultState: defaultAutoRefreshState, + decodeUrlState: decodeAutoRefreshUrlState, + encodeUrlState: urlAutoRefreshRT.encode, urlStateKey: AUTOREFRESH_URL_STATE_KEY, writeDefaultState: true, }); + useInterval( + () => { + handleQueryTimeRangeChange({ + start: urlTimeRange.startTime, + end: urlTimeRange.endTime, + }); + }, + autoRefresh.isPaused ? null : autoRefresh.interval + ); + return { - timeRange, + timeRange: queryTimeRange, + friendlyTimeRange: urlTimeRange, setTimeRange, autoRefresh, setAutoRefresh, }; }; + +const stringToNumericTimeRange = (timeRange: { start: string; end: string }): TimeRange => ({ + startTime: moment( + datemath.parse(timeRange.start, { + momentInstance: moment, + }) + ).valueOf(), + endTime: moment( + datemath.parse(timeRange.end, { + momentInstance: moment, + roundUp: true, + }) + ).valueOf(), +}); diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index b78912bfba3ac..b18b6e8a6eba6 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -24,6 +24,7 @@ import type { } from '../../observability/public'; import type { SpacesPluginStart } from '../../spaces/public'; import { MlPluginStart } from '../../ml/public'; +import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; // Our own setup and start contract values export type InfraClientSetupExports = void; @@ -46,6 +47,7 @@ export interface InfraClientStartDeps { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; usageCollection: UsageCollectionStart; ml: MlPluginStart; + embeddable?: EmbeddableStart; } export type InfraClientCoreSetup = CoreSetup; diff --git a/x-pack/plugins/infra/public/utils/use_url_state.ts b/x-pack/plugins/infra/public/utils/use_url_state.ts index fd927bb5ef662..970b3a20b2951 100644 --- a/x-pack/plugins/infra/public/utils/use_url_state.ts +++ b/x-pack/plugins/infra/public/utils/use_url_state.ts @@ -38,15 +38,13 @@ export const useUrlState = ({ return getParamFromQueryString(queryString, urlStateKey); }, [queryString, urlStateKey]); - const decodedState = useMemo(() => decodeUrlState(decodeRisonUrlState(urlStateString)), [ - decodeUrlState, - urlStateString, - ]); - - const state = useMemo(() => (typeof decodedState !== 'undefined' ? decodedState : defaultState), [ - defaultState, - decodedState, - ]); + const decodedState = useMemo(() => { + return decodeUrlState(decodeRisonUrlState(urlStateString)); + }, [decodeUrlState, urlStateString]); + + const state = useMemo(() => { + return typeof decodedState !== 'undefined' ? decodedState : defaultState; + }, [defaultState, decodedState]); const setState = useCallback( (newState: State | undefined) => { diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 00ec36d866624..8a6f22d55750e 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -12,7 +12,6 @@ import { initGetLogEntryCategoryDatasetsRoute, initGetLogEntryCategoryDatasetsStatsRoute, initGetLogEntryCategoryExamplesRoute, - initGetLogEntryRateRoute, initGetLogEntryExamplesRoute, initValidateLogAnalysisDatasetsRoute, initValidateLogAnalysisIndicesRoute, @@ -46,7 +45,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initGetLogEntryCategoryDatasetsRoute(libs); initGetLogEntryCategoryDatasetsStatsRoute(libs); initGetLogEntryCategoryExamplesRoute(libs); - initGetLogEntryRateRoute(libs); initGetLogEntryAnomaliesRoute(libs); initGetLogEntryAnomaliesDatasetsRoute(libs); initGetK8sAnomaliesRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts index 3fc098bcf8846..f5465a967f2a5 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts @@ -281,7 +281,6 @@ async function fetchLogEntryAnomalies( nextPageCursor: hits[hits.length - 1].sort, } : undefined; - const anomalies = hits.map((result) => { const { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts index d50495689e9d8..23c2ce5f0c21f 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts @@ -9,7 +9,6 @@ export * from './log_entry_categories'; export * from './log_entry_category_datasets'; export * from './log_entry_category_datasets_stats'; export * from './log_entry_category_examples'; -export * from './log_entry_rate'; export * from './log_entry_examples'; export * from './log_entry_anomalies'; export * from './log_entry_anomalies_datasets'; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts deleted file mode 100644 index c1762f88a6cdd..0000000000000 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts +++ /dev/null @@ -1,90 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; -import { InfraBackendLibs } from '../../../lib/infra_types'; -import { - LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, - getLogEntryRateRequestPayloadRT, - getLogEntryRateSuccessReponsePayloadRT, - GetLogEntryRateSuccessResponsePayload, -} from '../../../../common/http_api/log_analysis'; -import { createValidationFunction } from '../../../../common/runtime_types'; -import { getLogEntryRateBuckets } from '../../../lib/log_analysis'; -import { assertHasInfraMlPlugins } from '../../../utils/request_context'; -import { isMlPrivilegesError } from '../../../lib/log_analysis/errors'; - -export const initGetLogEntryRateRoute = ({ framework }: InfraBackendLibs) => { - framework.registerRoute( - { - method: 'post', - path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, - validate: { - body: createValidationFunction(getLogEntryRateRequestPayloadRT), - }, - }, - framework.router.handleLegacyErrors(async (requestContext, request, response) => { - const { - data: { sourceId, timeRange, bucketDuration, datasets }, - } = request.body; - - try { - assertHasInfraMlPlugins(requestContext); - - const logEntryRateBuckets = await getLogEntryRateBuckets( - requestContext, - sourceId, - timeRange.startTime, - timeRange.endTime, - bucketDuration, - datasets - ); - - return response.ok({ - body: getLogEntryRateSuccessReponsePayloadRT.encode({ - data: { - bucketDuration, - histogramBuckets: logEntryRateBuckets, - totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets), - }, - }), - }); - } catch (error) { - if (Boom.isBoom(error)) { - throw error; - } - - if (isMlPrivilegesError(error)) { - return response.customError({ - statusCode: 403, - body: { - message: error.message, - }, - }); - } - - return response.customError({ - statusCode: error.statusCode ?? 500, - body: { - message: error.message ?? 'An unexpected error occurred', - }, - }); - } - }) - ); -}; - -const getTotalNumberOfLogEntries = ( - logEntryRateBuckets: GetLogEntryRateSuccessResponsePayload['data']['histogramBuckets'] -) => { - return logEntryRateBuckets.reduce((sumNumberOfLogEntries, bucket) => { - const sumPartitions = bucket.partitions.reduce((partitionsTotal, partition) => { - return (partitionsTotal += partition.numberOfLogEntries); - }, 0); - return (sumNumberOfLogEntries += sumPartitions); - }, 0); -}; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts index 0d75db64a01b9..fa0cccda99d22 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts @@ -36,7 +36,7 @@ import { parseInterval } from '../../../common/util/parse_interval'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; -import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../ui_actions/apply_influencer_filters_action'; +import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../ui_actions/constants'; import { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableOutput, diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 1c4aa4031171d..c88ce2d7f95d2 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -39,8 +39,18 @@ export type { RenderCellValue, } from './shared'; +export type { AnomalySwimlaneEmbeddableInput } from './embeddables'; + +export { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from './embeddables/constants'; +export { CONTROLLED_BY_SWIM_LANE_FILTER } from './ui_actions/constants'; + // Static exports -export { getSeverityColor, getSeverityType } from '../common/util/anomaly_utils'; +export { + getSeverityColor, + getSeverityType, + getFormattedSeverityScore, +} from '../common/util/anomaly_utils'; + export { ANOMALY_SEVERITY } from '../common'; export { useMlHref, ML_PAGES, MlUrlGenerator } from './ml_url_generator'; diff --git a/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx b/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx index e9b70ee14aae6..e3d2ca4ce0de1 100644 --- a/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx @@ -11,11 +11,10 @@ import { MlCoreSetup } from '../plugin'; import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from '../application/explorer/explorer_constants'; import { Filter, FilterStateStore } from '../../../../../src/plugins/data/common'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, SwimLaneDrilldownContext } from '../embeddables'; +import { CONTROLLED_BY_SWIM_LANE_FILTER } from './constants'; export const APPLY_INFLUENCER_FILTERS_ACTION = 'applyInfluencerFiltersAction'; -export const CONTROLLED_BY_SWIM_LANE_FILTER = 'anomaly-swim-lane'; - export function createApplyInfluencerFiltersAction( getStartServices: MlCoreSetup['getStartServices'] ) { diff --git a/x-pack/plugins/ml/public/ui_actions/constants.ts b/x-pack/plugins/ml/public/ui_actions/constants.ts new file mode 100644 index 0000000000000..6dc3f03d10fd9 --- /dev/null +++ b/x-pack/plugins/ml/public/ui_actions/constants.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const CONTROLLED_BY_SWIM_LANE_FILTER = 'anomaly-swim-lane'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 439b9a93d0d97..278294dea9449 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9893,7 +9893,6 @@ "xpack.infra.logs.analysis.anomaliesExpandedRowActualRateTitle": "{actualCount, plural, other {件のメッセージ}}", "xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateDescription": "通常", "xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateTitle": "{typicalCount, plural, other {件のメッセージ}}", - "xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "15 分ごとのログエントリー (平均)", "xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "異常を読み込み中", "xpack.infra.logs.analysis.anomaliesSectionTitle": "異常", "xpack.infra.logs.analysis.anomaliesTableAnomalyDatasetName": "データセット", @@ -9905,7 +9904,6 @@ "xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage": "この{type, select, logRate {データセット} logCategory {カテゴリ}}のログメッセージ数が想定よりも多くなっています", "xpack.infra.logs.analysis.anomaliesTableNextPageLabel": "次のページ", "xpack.infra.logs.analysis.anomaliesTablePreviousPageLabel": "前のページ", - "xpack.infra.logs.analysis.anomalySectionLogRateChartNoData": "表示するログレートデータがありません。", "xpack.infra.logs.analysis.anomalySectionNoDataBody": "時間範囲を調整する必要があるかもしれません。", "xpack.infra.logs.analysis.anomalySectionNoDataTitle": "表示するデータがありません。", "xpack.infra.logs.analysis.createJobButtonLabel": "MLジョブを作成", @@ -9934,8 +9932,6 @@ "xpack.infra.logs.analysis.mlUnavailableTitle": "この機能には機械学習が必要です", "xpack.infra.logs.analysis.onboardingSuccessContent": "機械学習ロボットがデータの収集を開始するまでしばらくお待ちください。", "xpack.infra.logs.analysis.onboardingSuccessTitle": "成功!", - "xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel": "最高異常スコア", - "xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel": "最大異常スコア:{maxAnomalyScore}", "xpack.infra.logs.analysis.recreateJobButtonLabel": "ML ジョブを再作成", "xpack.infra.logs.analysis.setupFlyoutGotoListButtonLabel": "すべての機械学習ジョブ", "xpack.infra.logs.analysis.setupFlyoutTitle": "機械学習を使用した異常検知", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 643192df99309..4704cb07d27b0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9919,7 +9919,6 @@ "xpack.infra.logs.analysis.anomaliesExpandedRowActualRateTitle": "{actualCount, plural, other {消息}}", "xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateDescription": "典型", "xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateTitle": "{typicalCount, plural, other {消息}}", - "xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "每 15 分钟日志条目数(平均值)", "xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "正在加载异常", "xpack.infra.logs.analysis.anomaliesSectionTitle": "异常", "xpack.infra.logs.analysis.anomaliesTableAnomalyDatasetName": "数据集", @@ -9931,7 +9930,6 @@ "xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage": "此{type, select, logRate {数据集} logCategory {类别}}中的日志消息多于预期", "xpack.infra.logs.analysis.anomaliesTableNextPageLabel": "下一页", "xpack.infra.logs.analysis.anomaliesTablePreviousPageLabel": "上一页", - "xpack.infra.logs.analysis.anomalySectionLogRateChartNoData": "没有要显示的日志速率数据。", "xpack.infra.logs.analysis.anomalySectionNoDataBody": "您可能想调整时间范围。", "xpack.infra.logs.analysis.anomalySectionNoDataTitle": "没有可显示的数据。", "xpack.infra.logs.analysis.createJobButtonLabel": "创建 ML 作业", @@ -9960,8 +9958,6 @@ "xpack.infra.logs.analysis.mlUnavailableTitle": "此功能需要 Machine Learning", "xpack.infra.logs.analysis.onboardingSuccessContent": "请注意,我们的 Machine Learning 机器人若干分钟后才会开始收集数据。", "xpack.infra.logs.analysis.onboardingSuccessTitle": "成功!", - "xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel": "最大异常分数:", - "xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel": "最大异常分数:{maxAnomalyScore}", "xpack.infra.logs.analysis.recreateJobButtonLabel": "重新创建 ML 作业", "xpack.infra.logs.analysis.setupFlyoutGotoListButtonLabel": "所有 Machine Learning 作业", "xpack.infra.logs.analysis.setupFlyoutTitle": "通过 Machine Learning 检测异常", diff --git a/x-pack/test/api_integration/apis/metrics_ui/index.js b/x-pack/test/api_integration/apis/metrics_ui/index.js index 254360ce64922..34ad92e6b89a6 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/index.js +++ b/x-pack/test/api_integration/apis/metrics_ui/index.js @@ -8,7 +8,6 @@ export default function ({ loadTestFile }) { describe('MetricsUI Endpoints', () => { loadTestFile(require.resolve('./metadata')); - loadTestFile(require.resolve('./log_analysis')); loadTestFile(require.resolve('./log_entries')); loadTestFile(require.resolve('./log_entry_highlights')); loadTestFile(require.resolve('./logs_without_millis')); diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts b/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts deleted file mode 100644 index ecfa0cc6f2438..0000000000000 --- a/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts +++ /dev/null @@ -1,137 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { identity } from 'fp-ts/lib/function'; -import { fold } from 'fp-ts/lib/Either'; -import { - LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, - getLogEntryRateRequestPayloadRT, - getLogEntryRateSuccessReponsePayloadRT, -} from '../../../../plugins/infra/common/http_api/log_analysis'; -import { createPlainError, throwErrors } from '../../../../plugins/infra/common/runtime_types'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -const TIME_BEFORE_START = 1569934800000; -const TIME_AFTER_END = 1570016700000; -const COMMON_HEADERS = { - 'kbn-xsrf': 'some-xsrf-token', -}; -const ML_JOB_ID = 'kibana-logs-ui-default-default-log-entry-rate'; - -export default ({ getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - const retry = getService('retry'); - - async function createDummyJob(jobId: string) { - await supertest - .put(`/api/ml/anomaly_detectors/${jobId}`) - .set(COMMON_HEADERS) - .send({ - job_id: jobId, - groups: [], - analysis_config: { - bucket_span: '15m', - detectors: [{ function: 'count' }], - influencers: [], - }, - data_description: { time_field: '@timestamp' }, - analysis_limits: { model_memory_limit: '11MB' }, - model_plot_config: { enabled: false, annotations_enabled: false }, - }) - .expect(200); - } - - async function deleteDummyJob(jobId: string) { - await supertest.delete(`/api/ml/anomaly_detectors/${jobId}`).set(COMMON_HEADERS).expect(200); - - await retry.waitForWithTimeout(`'${jobId}' to not exist`, 5 * 1000, async () => { - if (await supertest.get(`/api/ml/anomaly_detectors/${jobId}`).expect(404)) { - return true; - } else { - throw new Error(`expected anomaly detection job '${jobId}' not to exist`); - } - }); - } - - describe('log analysis apis', () => { - before(async () => { - // a real ML job must exist when searching for the results - await createDummyJob(ML_JOB_ID); - await esArchiver.load('infra/8.0.0/ml_anomalies_partitioned_log_rate'); - }); - after(async () => { - await deleteDummyJob(ML_JOB_ID); - await esArchiver.unload('infra/8.0.0/ml_anomalies_partitioned_log_rate'); - }); - - describe('log rate results', () => { - describe('with the default source', () => { - it('should return buckets when there are matching ml result documents', async () => { - const { body } = await supertest - .post(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH) - .set(COMMON_HEADERS) - .send( - getLogEntryRateRequestPayloadRT.encode({ - data: { - sourceId: 'default', - timeRange: { - startTime: TIME_BEFORE_START, - endTime: TIME_AFTER_END, - }, - bucketDuration: 15 * 60 * 1000, - }, - }) - ) - .expect(200); - - const logEntryRateBuckets = pipe( - getLogEntryRateSuccessReponsePayloadRT.decode(body), - fold(throwErrors(createPlainError), identity) - ); - expect(logEntryRateBuckets.data.bucketDuration).to.be(15 * 60 * 1000); - expect(logEntryRateBuckets.data.histogramBuckets).to.not.be.empty(); - expect( - logEntryRateBuckets.data.histogramBuckets.some((bucket) => { - return bucket.partitions.some((partition) => partition.anomalies.length > 0); - }) - ).to.be(true); - }); - - it('should return no buckets when there are no matching ml result documents', async () => { - const { body } = await supertest - .post(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH) - .set(COMMON_HEADERS) - .send( - getLogEntryRateRequestPayloadRT.encode({ - data: { - sourceId: 'default', - timeRange: { - startTime: TIME_BEFORE_START - 10 * 15 * 60 * 1000, - endTime: TIME_BEFORE_START - 1, - }, - bucketDuration: 15 * 60 * 1000, - }, - }) - ) - .expect(200); - - const logEntryRateBuckets = pipe( - getLogEntryRateSuccessReponsePayloadRT.decode(body), - fold(throwErrors(createPlainError), identity) - ); - - expect(logEntryRateBuckets.data.bucketDuration).to.be(15 * 60 * 1000); - expect(logEntryRateBuckets.data.histogramBuckets).to.be.empty(); - }); - }); - }); - }); -}; From 5099eab19f12271b935efe47b8ed6ea508f8e00c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 9 Feb 2021 16:29:16 +0100 Subject: [PATCH 060/100] [ILM] Simplify timeline and rollover info (#90004) * remove rollover indicator on timeline and relative timing text * simplify rollover description text * remove non-existent export * incorporate copy recommendations * slight visual adjustment to timeline, infinity icon more subdued, moved icon to left * update visual appearance of delete icon on timeline, grey circle and trash can * remove tooltip next to recommended defaults Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../edit_policy/edit_policy.helpers.tsx | 1 - .../edit_policy/edit_policy.test.ts | 8 - .../components/phases/hot_phase/hot_phase.tsx | 49 ++-- .../phases/shared_fields/forcemerge_field.tsx | 2 +- .../searchable_snapshot_field.tsx | 2 +- .../phases/shared_fields/shrink_field.tsx | 2 +- .../components/timeline_phase_text.tsx | 4 +- .../components/timeline/timeline.scss | 18 +- .../components/timeline/timeline.tsx | 48 ++-- .../sections/edit_policy/i18n_texts.ts | 4 +- ...absolute_timing_to_relative_timing.test.ts | 240 ------------------ .../lib/absolute_timing_to_relative_timing.ts | 52 ---- .../sections/edit_policy/lib/index.ts | 2 - 13 files changed, 58 insertions(+), 374 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index dc375f6370048..38049dd7c6cfa 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -278,7 +278,6 @@ export const setup = async (arg?: { appServicesContext: Partial exists('policyFormErrorsCallout'), timeline: { - hasRolloverIndicator: () => exists('timelineHotPhaseRolloverToolTip'), hasHotPhase: () => exists('ilmTimelineHotPhase'), hasWarmPhase: () => exists('ilmTimelineWarmPhase'), hasColdPhase: () => exists('ilmTimelineColdPhase'), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index f2266741ec7d1..282daf780b86c 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -845,14 +845,6 @@ describe('', () => { expect(actions.timeline.hasColdPhase()).toBe(true); expect(actions.timeline.hasDeletePhase()).toBe(true); }); - - test('show and hide rollover indicator on timeline', async () => { - const { actions } = testBed; - expect(actions.timeline.hasRolloverIndicator()).toBe(true); - await actions.hot.toggleDefaultRollover(false); - await actions.hot.toggleRollover(false); - expect(actions.timeline.hasRolloverIndicator()).toBe(false); - }); }); describe('policy error notifications', () => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index 74809965a52d9..c77493476b929 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -17,7 +17,7 @@ import { EuiTextColor, EuiSwitch, EuiIconTip, - EuiIcon, + EuiText, } from '@elastic/eui'; import { useFormData, SelectField, NumericField } from '../../../../../../shared_imports'; @@ -68,8 +68,20 @@ export const HotPhase: FunctionComponent = () => {

{' '} + defaultMessage="Start writing to a new index when the current index reaches a certain size, document count, or age. Enables you to optimize performance and manage resource usage when working with time series data." + /> +

+ + + +

+ + {i18n.translate( + 'xpack.indexLifecycleMgmt.rollover.rolloverOffsetsPhaseTimingDescriptionNote', + { defaultMessage: 'Note: ' } + )} + + {i18nTexts.editPolicy.rolloverOffsetsHotPhaseTiming}{' '} {

- -   - {i18nTexts.editPolicy.rolloverOffsetsHotPhaseTiming} - path={isUsingDefaultRolloverPath}> {(field) => ( <> - field.setValue(e.target.checked)} - data-test-subj="useDefaultRolloverSwitch" - /> -   - - } + + field.setValue(e.target.checked)} + data-test-subj="useDefaultRolloverSwitch" + /> + + + )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx index bbdcbbf4759ef..8cb566ceae25a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx @@ -45,7 +45,7 @@ export const ForcemergeField: React.FunctionComponent = ({ phase }) => { <> {' '} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 9251b08742476..c85201f708a2b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -342,7 +342,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => , }} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx index b5fb79811ee2d..8ac387ba106b7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx @@ -38,7 +38,7 @@ export const ShrinkField: FunctionComponent = ({ phase }) => { {' '} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx index 3a9f33fa3d169..62b100b85cbe2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx @@ -12,8 +12,8 @@ export const TimelinePhaseText: FunctionComponent<{ phaseName: ReactNode | string; durationInPhase?: ReactNode | string; }> = ({ phaseName, durationInPhase }) => ( - - + + {phaseName} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss index 7d65d2cd6b212..de49e665ed933 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss @@ -1,11 +1,5 @@ $ilmTimelineBarHeight: $euiSizeS; -/* -* For theming we need to shade or tint to get the right color from the base EUI color -*/ -$ilmDeletePhaseBackgroundColor: tintOrShade($euiColorVis5_behindText, 80%,80%); -$ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%); - .ilmTimeline { overflow: hidden; width: 100%; @@ -49,14 +43,16 @@ $ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%); */ padding: $euiSizeM; margin-left: $euiSizeM; - background-color: $ilmDeletePhaseBackgroundColor; - color: $ilmDeletePhaseColor; - border-radius: calc(#{$euiSizeS} / 2); + background-color: $euiColorLightestShade; + color: $euiColorDarkShade; + border-radius: 50%; } &__colorBar { display: inline-block; height: $ilmTimelineBarHeight; + margin-top: $euiSizeS; + margin-bottom: $euiSizeXS; border-radius: calc(#{$ilmTimelineBarHeight} / 2); width: 100%; } @@ -84,8 +80,4 @@ $ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%); background-color: $euiColorVis1; } } - - &__rolloverIcon { - display: inline-block; - } } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx index 2d83009bd4df4..8097ab51eb59e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx @@ -8,14 +8,12 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent, memo } from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiIconTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiIconTip } from '@elastic/eui'; import { PhasesExceptDelete } from '../../../../../../common/types'; import { calculateRelativeFromAbsoluteMilliseconds, - normalizeTimingsToHumanReadable, PhaseAgeInMilliseconds, AbsoluteTimings, } from '../../lib'; @@ -48,6 +46,12 @@ const msTimeToOverallPercent = (ms: number, totalMs: number) => { const SCORE_BUFFER_AMOUNT = 50; const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.timeline.title', { + defaultMessage: 'Policy Summary', + }), + description: i18n.translate('xpack.indexLifecycleMgmt.timeline.description', { + defaultMessage: 'This policy moves data through the following phases.', + }), hotPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.hotPhaseSectionTitle', { defaultMessage: 'Hot phase', }), @@ -69,6 +73,11 @@ const i18nTexts = { defaultMessage: 'Policy deletes the index after lifecycle phases complete.', }), }, + foreverIcon: { + ariaLabel: i18n.translate('xpack.indexLifecycleMgmt.timeline.foreverIconToolTipContent', { + defaultMessage: 'Forever', + }), + }, }; const calculateWidths = (inputs: PhaseAgeInMilliseconds) => { @@ -118,27 +127,23 @@ export const Timeline: FunctionComponent = memo( }; const phaseAgeInMilliseconds = calculateRelativeFromAbsoluteMilliseconds(absoluteTimings); - const humanReadableTimings = normalizeTimingsToHumanReadable(phaseAgeInMilliseconds); const widths = calculateWidths(phaseAgeInMilliseconds); const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode => phaseAgeInMilliseconds.phases[phase] === Infinity ? ( - - ) : ( - humanReadableTimings[phase] - ); + + ) : null; return ( -

- {i18n.translate('xpack.indexLifecycleMgmt.timeline.title', { - defaultMessage: 'Policy Timeline', - })} -

+

{i18nTexts.title}

+ + {i18nTexts.description} +
= memo( >
- {i18nTexts.hotPhase} -   -
- -
- - ) : ( - i18nTexts.hotPhase - ) - } + phaseName={i18nTexts.hotPhase} durationInPhase={getDurationInPhaseContent('hot')} />
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 5deba8607cd52..3923cf93cd0d3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -16,7 +16,7 @@ export const i18nTexts = { 'xpack.indexLifecycleMgmt.rollover.rolloverOffsetsPhaseTimingDescription', { defaultMessage: - 'How long it takes to reach the rollover criteria in the hot phase can vary. Data moves to the next phase when the time since rollover reaches the minimum age.', + 'How long it takes to reach the rollover criteria in the hot phase can vary.', } ), searchableSnapshotInHotPhase: { @@ -195,7 +195,7 @@ export const i18nTexts = { descriptions: { hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescription', { defaultMessage: - 'This phase is required. You are actively querying and writing to your index. For faster updates, you can roll over the index when it gets too big or too old.', + 'You actively store and query data in the hot phase. All policies have a hot phase.', }), warm: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescription', { defaultMessage: diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts index 7ec20cc2a5966..8a9635e2db219 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts @@ -11,7 +11,6 @@ import { deserializer } from '../form'; import { formDataToAbsoluteTimings, calculateRelativeFromAbsoluteMilliseconds, - absoluteTimingToRelativeTiming, } from './absolute_timing_to_relative_timing'; export const calculateRelativeTimingMs = flow( @@ -273,243 +272,4 @@ describe('Conversion of absolute policy timing to relative timing', () => { }); }); }); - - describe('absoluteTimingToRelativeTiming', () => { - describe('policy that never deletes data (keep forever)', () => { - test('always hot', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - }, - }) - ) - ).toEqual({ total: 'forever', hot: 'forever', warm: undefined, cold: undefined }); - }); - - test('hot, then always warm', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - warm: { - actions: {}, - }, - }, - }) - ) - ).toEqual({ total: 'forever', hot: 'less than a day', warm: 'forever', cold: undefined }); - }); - - test('hot, then warm, then always cold', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - warm: { - min_age: '1M', - actions: {}, - }, - cold: { - min_age: '34d', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: 'forever', - hot: '30 days', - warm: '4 days', - cold: 'forever', - }); - }); - - test('hot, then always cold', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - cold: { - min_age: '34d', - actions: {}, - }, - }, - }) - ) - ).toEqual({ total: 'forever', hot: '34 days', warm: undefined, cold: 'forever' }); - }); - }); - - describe('policy that deletes data', () => { - test('hot, then delete', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - delete: { - min_age: '1M', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: '30 days', - hot: '30 days', - warm: undefined, - cold: undefined, - }); - }); - - test('hot, then warm, then delete', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - warm: { - min_age: '24d', - actions: {}, - }, - delete: { - min_age: '1M', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: '30 days', - hot: '24 days', - warm: '6 days', - cold: undefined, - }); - }); - - test('hot, then warm, then cold, then delete', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - warm: { - min_age: '24d', - actions: {}, - }, - cold: { - min_age: '2M', - actions: {}, - }, - delete: { - min_age: '2d', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: '61 days', - hot: '24 days', - warm: '37 days', - cold: 'less than a day', - }); - }); - - test('hot, then cold, then delete', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - cold: { - min_age: '2M', - actions: {}, - }, - delete: { - min_age: '2d', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: '61 days', - hot: '61 days', - warm: undefined, - cold: 'less than a day', - }); - }); - - test('hot, then long warm, then short cold, then delete', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - warm: { - min_age: '2M', - actions: {}, - }, - cold: { - min_age: '1d', - actions: {}, - }, - delete: { - min_age: '2d', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: '61 days', - hot: '61 days', - warm: 'less than a day', - cold: 'less than a day', - }); - }); - }); - }); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts index 73ff8c76b9233..2974a88c22343 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts @@ -21,8 +21,6 @@ */ import moment from 'moment'; -import { i18n } from '@kbn/i18n'; -import { flow } from 'fp-ts/function'; import { splitSizeAndUnits } from '../../../lib/policies'; @@ -34,21 +32,6 @@ type MinAgePhase = 'warm' | 'cold' | 'delete'; type Phase = 'hot' | MinAgePhase; -const i18nTexts = { - forever: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.forever', { - defaultMessage: 'forever', - }), - lessThanADay: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.lessThanADay', { - defaultMessage: 'less than a day', - }), - day: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.day', { - defaultMessage: 'day', - }), - days: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.days', { - defaultMessage: 'days', - }), -}; - const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete']; const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({ @@ -162,38 +145,3 @@ export const calculateRelativeFromAbsoluteMilliseconds = ( }; export type RelativePhaseTimingInMs = ReturnType; - -const millisecondsToDays = (milliseconds?: number): string | undefined => { - if (milliseconds == null) { - return; - } - if (!isFinite(milliseconds)) { - return i18nTexts.forever; - } - const days = milliseconds / 8.64e7; - return days < 1 - ? i18nTexts.lessThanADay - : `${Math.floor(days)} ${days === 1 ? i18nTexts.day : i18nTexts.days}`; -}; - -export const normalizeTimingsToHumanReadable = ({ - total, - phases, -}: PhaseAgeInMilliseconds): { total?: string; hot?: string; warm?: string; cold?: string } => { - return { - total: millisecondsToDays(total), - hot: millisecondsToDays(phases.hot), - warm: millisecondsToDays(phases.warm), - cold: millisecondsToDays(phases.cold), - }; -}; - -/** - * Given {@link FormInternal}, extract the min_age values for each phase and calculate - * human readable strings for communicating how long data will remain in a phase. - */ -export const absoluteTimingToRelativeTiming = flow( - formDataToAbsoluteTimings, - calculateRelativeFromAbsoluteMilliseconds, - normalizeTimingsToHumanReadable -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts index 396318a1d78cf..af4757a7b7105 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts @@ -6,9 +6,7 @@ */ export { - absoluteTimingToRelativeTiming, calculateRelativeFromAbsoluteMilliseconds, - normalizeTimingsToHumanReadable, formDataToAbsoluteTimings, AbsoluteTimings, PhaseAgeInMilliseconds, From 3cb04fc6d080c9158026885c2fab6a6062fe48b3 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 9 Feb 2021 10:46:07 -0500 Subject: [PATCH 061/100] Expand Tinymath grammar, including named arguments (#89795) * Expand Tinymath grammar, including named arguments * Add tsconfig project * Fix tests * Allow named arguments with numeric types * Remove dashes from named argument validation * Fix license header Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + packages/kbn-tinymath/package.json | 1 + packages/kbn-tinymath/src/grammar.js | 602 +++++++++++------- packages/kbn-tinymath/src/grammar.pegjs | 83 ++- packages/kbn-tinymath/src/index.js | 28 +- packages/kbn-tinymath/test/library.test.js | 192 ++++-- packages/kbn-tinymath/tinymath.d.ts | 45 ++ packages/kbn-tinymath/tsconfig.json | 7 + .../response_processors/series/math.test.js | 4 +- .../functions/common/math.ts | 1 - ...pe.test.js => get_expression_type.test.ts} | 2 +- .../functions/server/get_field_names.test.ts | 1 - .../functions/server/pointseries/index.ts | 15 +- ...ression_type.js => get_expression_type.ts} | 7 +- .../server/pointseries/lib/get_field_names.ts | 22 +- .../pointseries/lib/is_column_reference.ts | 3 +- ...object.test.js => get_form_object.test.ts} | 0 ...{get_form_object.js => get_form_object.ts} | 17 +- 18 files changed, 683 insertions(+), 348 deletions(-) create mode 100644 packages/kbn-tinymath/tinymath.d.ts create mode 100644 packages/kbn-tinymath/tsconfig.json rename x-pack/plugins/canvas/canvas_plugin_src/functions/server/{get_expression_type.test.js => get_expression_type.test.ts} (96%) rename x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/{get_expression_type.js => get_expression_type.ts} (82%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/{get_form_object.test.js => get_form_object.test.ts} (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/{get_form_object.js => get_form_object.ts} (71%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 34b449346ddf7..87dc99fa33749 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -26,6 +26,7 @@ /src/plugins/vis_type_xy/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app /src/plugins/visualizations/ @elastic/kibana-app +/packages/kbn-tinymath/ @elastic/kibana-app # Application Services /examples/bfetch_explorer/ @elastic/kibana-app-services diff --git a/packages/kbn-tinymath/package.json b/packages/kbn-tinymath/package.json index 13b77b1482af9..cc4fa0a64d9c3 100644 --- a/packages/kbn-tinymath/package.json +++ b/packages/kbn-tinymath/package.json @@ -4,6 +4,7 @@ "license": "SSPL-1.0 OR Elastic License 2.0", "private": true, "main": "src/index.js", + "types": "tinymath.d.ts", "scripts": { "kbn:bootstrap": "yarn build", "build": "../../node_modules/.bin/pegjs -o src/grammar.js src/grammar.pegjs" diff --git a/packages/kbn-tinymath/src/grammar.js b/packages/kbn-tinymath/src/grammar.js index 60dfcf4800631..5454143530c39 100644 --- a/packages/kbn-tinymath/src/grammar.js +++ b/packages/kbn-tinymath/src/grammar.js @@ -156,11 +156,21 @@ function peg$parse(input, options) { peg$c12 = function(literal) { return literal; }, - peg$c13 = function(first, rest) { // We can open this up later. Strict for now. - return first + rest.join(''); + peg$c13 = function(chars) { + return { + type: 'variable', + value: chars.join(''), + location: simpleLocation(location()), + text: text() + }; }, - peg$c14 = function(first, mid) { - return first + mid.map(m => m[0].join('') + m[1].join('')).join('') + peg$c14 = function(rest) { + return { + type: 'variable', + value: rest.join(''), + location: simpleLocation(location()), + text: text() + }; }, peg$c15 = "+", peg$c16 = peg$literalExpectation("+", false), @@ -168,8 +178,11 @@ function peg$parse(input, options) { peg$c18 = peg$literalExpectation("-", false), peg$c19 = function(left, rest) { return rest.reduce((acc, curr) => ({ + type: 'function', name: curr[0] === '+' ? 'add' : 'subtract', - args: [acc, curr[1]] + args: [acc, curr[1]], + location: simpleLocation(location()), + text: text() }), left) }, peg$c20 = "*", @@ -178,8 +191,11 @@ function peg$parse(input, options) { peg$c23 = peg$literalExpectation("/", false), peg$c24 = function(left, rest) { return rest.reduce((acc, curr) => ({ + type: 'function', name: curr[0] === '*' ? 'multiply' : 'divide', - args: [acc, curr[1]] + args: [acc, curr[1]], + location: simpleLocation(location()), + text: text() }), left) }, peg$c25 = "(", @@ -196,25 +212,51 @@ function peg$parse(input, options) { peg$c34 = function(first, rest) { return [first].concat(rest); }, - peg$c35 = peg$otherExpectation("function"), - peg$c36 = /^[a-z]/, - peg$c37 = peg$classExpectation([["a", "z"]], false, false), - peg$c38 = function(name, args) { - return {name: name.join(''), args: args || []}; + peg$c35 = /^["]/, + peg$c36 = peg$classExpectation(["\""], false, false), + peg$c37 = function(value) { return value.join(''); }, + peg$c38 = /^[']/, + peg$c39 = peg$classExpectation(["'"], false, false), + peg$c40 = /^[a-zA-Z_]/, + peg$c41 = peg$classExpectation([["a", "z"], ["A", "Z"], "_"], false, false), + peg$c42 = "=", + peg$c43 = peg$literalExpectation("=", false), + peg$c44 = function(name, value) { + return { + type: 'namedArgument', + name: name.join(''), + value: value, + location: simpleLocation(location()), + text: text() + }; + }, + peg$c45 = peg$otherExpectation("function"), + peg$c46 = /^[a-zA-Z_\-]/, + peg$c47 = peg$classExpectation([["a", "z"], ["A", "Z"], "_", "-"], false, false), + peg$c48 = function(name, args) { + return { + type: 'function', + name: name.join(''), + args: args || [], + location: simpleLocation(location()), + text: text() + }; }, - peg$c39 = peg$otherExpectation("number"), - peg$c40 = function() { return parseFloat(text()); }, - peg$c41 = /^[eE]/, - peg$c42 = peg$classExpectation(["e", "E"], false, false), - peg$c43 = peg$otherExpectation("exponent"), - peg$c44 = ".", - peg$c45 = peg$literalExpectation(".", false), - peg$c46 = "0", - peg$c47 = peg$literalExpectation("0", false), - peg$c48 = /^[1-9]/, - peg$c49 = peg$classExpectation([["1", "9"]], false, false), - peg$c50 = /^[0-9]/, - peg$c51 = peg$classExpectation([["0", "9"]], false, false), + peg$c49 = peg$otherExpectation("number"), + peg$c50 = function() { + return parseFloat(text()); + }, + peg$c51 = /^[eE]/, + peg$c52 = peg$classExpectation(["e", "E"], false, false), + peg$c53 = peg$otherExpectation("exponent"), + peg$c54 = ".", + peg$c55 = peg$literalExpectation(".", false), + peg$c56 = "0", + peg$c57 = peg$literalExpectation("0", false), + peg$c58 = /^[1-9]/, + peg$c59 = peg$classExpectation([["1", "9"]], false, false), + peg$c60 = /^[0-9]/, + peg$c61 = peg$classExpectation([["0", "9"]], false, false), peg$currPos = 0, peg$savedPos = 0, @@ -456,10 +498,7 @@ function peg$parse(input, options) { if (s1 !== peg$FAILED) { s2 = peg$parseNumber(); if (s2 === peg$FAILED) { - s2 = peg$parseVariableWithQuote(); - if (s2 === peg$FAILED) { - s2 = peg$parseVariable(); - } + s2 = peg$parseVariable(); } if (s2 !== peg$FAILED) { s3 = peg$parse_(); @@ -489,25 +528,37 @@ function peg$parse(input, options) { } function peg$parseVariable() { - var s0, s1, s2, s3, s4; + var s0, s1, s2, s3, s4, s5; s0 = peg$currPos; s1 = peg$parse_(); if (s1 !== peg$FAILED) { - s2 = peg$parseStartChar(); + s2 = peg$parseQuote(); if (s2 !== peg$FAILED) { s3 = []; s4 = peg$parseValidChar(); + if (s4 === peg$FAILED) { + s4 = peg$parseSpace(); + } while (s4 !== peg$FAILED) { s3.push(s4); s4 = peg$parseValidChar(); + if (s4 === peg$FAILED) { + s4 = peg$parseSpace(); + } } if (s3 !== peg$FAILED) { - s4 = peg$parse_(); + s4 = peg$parseQuote(); if (s4 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c13(s2, s3); - s0 = s1; + s5 = peg$parse_(); + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c13(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } } else { peg$currPos = s0; s0 = peg$FAILED; @@ -524,98 +575,26 @@ function peg$parse(input, options) { peg$currPos = s0; s0 = peg$FAILED; } - - return s0; - } - - function peg$parseVariableWithQuote() { - var s0, s1, s2, s3, s4, s5, s6, s7, s8; - - s0 = peg$currPos; - s1 = peg$parse_(); - if (s1 !== peg$FAILED) { - s2 = peg$parseQuote(); - if (s2 !== peg$FAILED) { - s3 = peg$parseStartChar(); + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseValidChar(); if (s3 !== peg$FAILED) { - s4 = []; - s5 = peg$currPos; - s6 = []; - s7 = peg$parseSpace(); - while (s7 !== peg$FAILED) { - s6.push(s7); - s7 = peg$parseSpace(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseValidChar(); } - if (s6 !== peg$FAILED) { - s7 = []; - s8 = peg$parseValidChar(); - if (s8 !== peg$FAILED) { - while (s8 !== peg$FAILED) { - s7.push(s8); - s8 = peg$parseValidChar(); - } - } else { - s7 = peg$FAILED; - } - if (s7 !== peg$FAILED) { - s6 = [s6, s7]; - s5 = s6; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - while (s5 !== peg$FAILED) { - s4.push(s5); - s5 = peg$currPos; - s6 = []; - s7 = peg$parseSpace(); - while (s7 !== peg$FAILED) { - s6.push(s7); - s7 = peg$parseSpace(); - } - if (s6 !== peg$FAILED) { - s7 = []; - s8 = peg$parseValidChar(); - if (s8 !== peg$FAILED) { - while (s8 !== peg$FAILED) { - s7.push(s8); - s8 = peg$parseValidChar(); - } - } else { - s7 = peg$FAILED; - } - if (s7 !== peg$FAILED) { - s6 = [s6, s7]; - s5 = s6; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - } - if (s4 !== peg$FAILED) { - s5 = peg$parseQuote(); - if (s5 !== peg$FAILED) { - s6 = peg$parse_(); - if (s6 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c14(s3, s4); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s3 = peg$parse_(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c14(s2); + s0 = s1; } else { peg$currPos = s0; s0 = peg$FAILED; @@ -628,9 +607,6 @@ function peg$parse(input, options) { peg$currPos = s0; s0 = peg$FAILED; } - } else { - peg$currPos = s0; - s0 = peg$FAILED; } return s0; @@ -911,105 +887,288 @@ function peg$parse(input, options) { return s0; } - function peg$parseArguments() { - var s0, s1, s2, s3, s4, s5, s6, s7, s8; + function peg$parseArgument_List() { + var s0, s1, s2, s3, s4, s5, s6, s7; peg$silentFails++; s0 = peg$currPos; - s1 = peg$parse_(); + s1 = peg$parseArgument(); if (s1 !== peg$FAILED) { - s2 = peg$parseAddSubtract(); - if (s2 !== peg$FAILED) { - s3 = []; - s4 = peg$currPos; - s5 = peg$parse_(); + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c31; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 44) { - s6 = peg$c31; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c32); } - } + s6 = peg$parse_(); if (s6 !== peg$FAILED) { - s7 = peg$parse_(); + s7 = peg$parseArgument(); if (s7 !== peg$FAILED) { - s8 = peg$parseAddSubtract(); - if (s8 !== peg$FAILED) { - peg$savedPos = s4; - s5 = peg$c33(s2, s8); - s4 = s5; - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } + peg$savedPos = s3; + s4 = peg$c33(s1, s7); + s3 = s4; } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s3; + s3 = peg$FAILED; } } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s3; + s3 = peg$FAILED; } } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s3; + s3 = peg$FAILED; } - while (s4 !== peg$FAILED) { - s3.push(s4); - s4 = peg$currPos; - s5 = peg$parse_(); + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c31; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 44) { - s6 = peg$c31; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c32); } - } + s6 = peg$parse_(); if (s6 !== peg$FAILED) { - s7 = peg$parse_(); + s7 = peg$parseArgument(); if (s7 !== peg$FAILED) { - s8 = peg$parseAddSubtract(); - if (s8 !== peg$FAILED) { - peg$savedPos = s4; - s5 = peg$c33(s2, s8); - s4 = s5; - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } + peg$savedPos = s3; + s4 = peg$c33(s1, s7); + s3 = s4; } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s3; + s3 = peg$FAILED; } } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s3; + s3 = peg$FAILED; } } else { - peg$currPos = s4; + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + if (s2 !== peg$FAILED) { + s3 = peg$parse_(); + if (s3 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c31; + peg$currPos++; + } else { s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c34(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + + return s0; + } + + function peg$parseString() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (peg$c35.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c36); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseValidChar(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseValidChar(); + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + if (peg$c35.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c36); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c37(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (peg$c38.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c39); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseValidChar(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseValidChar(); + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + if (peg$c38.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c39); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c37(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = []; + s2 = peg$parseValidChar(); + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$parseValidChar(); } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c37(s1); + } + s0 = s1; + } + } + + return s0; + } + + function peg$parseArgument() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = []; + if (peg$c40.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c40.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = peg$parse_(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 61) { + s3 = peg$c42; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c43); } } if (s3 !== peg$FAILED) { s4 = peg$parse_(); if (s4 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 44) { - s5 = peg$c31; - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c32); } - } + s5 = peg$parseNumber(); if (s5 === peg$FAILED) { - s5 = null; + s5 = peg$parseString(); } if (s5 !== peg$FAILED) { s6 = peg$parse_(); if (s6 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$c34(s2, s3); + s1 = peg$c44(s1, s5); s0 = s1; } else { peg$currPos = s0; @@ -1035,10 +1194,8 @@ function peg$parse(input, options) { peg$currPos = s0; s0 = peg$FAILED; } - peg$silentFails--; if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c30); } + s0 = peg$parseAddSubtract(); } return s0; @@ -1052,22 +1209,22 @@ function peg$parse(input, options) { s1 = peg$parse_(); if (s1 !== peg$FAILED) { s2 = []; - if (peg$c36.test(input.charAt(peg$currPos))) { + if (peg$c46.test(input.charAt(peg$currPos))) { s3 = input.charAt(peg$currPos); peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c37); } + if (peg$silentFails === 0) { peg$fail(peg$c47); } } if (s3 !== peg$FAILED) { while (s3 !== peg$FAILED) { s2.push(s3); - if (peg$c36.test(input.charAt(peg$currPos))) { + if (peg$c46.test(input.charAt(peg$currPos))) { s3 = input.charAt(peg$currPos); peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c37); } + if (peg$silentFails === 0) { peg$fail(peg$c47); } } } } else { @@ -1084,7 +1241,7 @@ function peg$parse(input, options) { if (s3 !== peg$FAILED) { s4 = peg$parse_(); if (s4 !== peg$FAILED) { - s5 = peg$parseArguments(); + s5 = peg$parseArgument_List(); if (s5 === peg$FAILED) { s5 = null; } @@ -1102,7 +1259,7 @@ function peg$parse(input, options) { s8 = peg$parse_(); if (s8 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$c38(s2, s5); + s1 = peg$c48(s2, s5); s0 = s1; } else { peg$currPos = s0; @@ -1139,7 +1296,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c35); } + if (peg$silentFails === 0) { peg$fail(peg$c45); } } return s0; @@ -1174,7 +1331,7 @@ function peg$parse(input, options) { } if (s4 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$c40(); + s1 = peg$c50(); s0 = s1; } else { peg$currPos = s0; @@ -1195,7 +1352,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c39); } + if (peg$silentFails === 0) { peg$fail(peg$c49); } } return s0; @@ -1204,12 +1361,12 @@ function peg$parse(input, options) { function peg$parseE() { var s0; - if (peg$c41.test(input.charAt(peg$currPos))) { + if (peg$c51.test(input.charAt(peg$currPos))) { s0 = input.charAt(peg$currPos); peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c42); } + if (peg$silentFails === 0) { peg$fail(peg$c52); } } return s0; @@ -1261,7 +1418,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c43); } + if (peg$silentFails === 0) { peg$fail(peg$c53); } } return s0; @@ -1272,11 +1429,11 @@ function peg$parse(input, options) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 46) { - s1 = peg$c44; + s1 = peg$c54; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c45); } + if (peg$silentFails === 0) { peg$fail(peg$c55); } } if (s1 !== peg$FAILED) { s2 = []; @@ -1308,20 +1465,20 @@ function peg$parse(input, options) { var s0, s1, s2, s3; if (input.charCodeAt(peg$currPos) === 48) { - s0 = peg$c46; + s0 = peg$c56; peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c47); } + if (peg$silentFails === 0) { peg$fail(peg$c57); } } if (s0 === peg$FAILED) { s0 = peg$currPos; - if (peg$c48.test(input.charAt(peg$currPos))) { + if (peg$c58.test(input.charAt(peg$currPos))) { s1 = input.charAt(peg$currPos); peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c49); } + if (peg$silentFails === 0) { peg$fail(peg$c59); } } if (s1 !== peg$FAILED) { s2 = []; @@ -1349,17 +1506,30 @@ function peg$parse(input, options) { function peg$parseDigit() { var s0; - if (peg$c50.test(input.charAt(peg$currPos))) { + if (peg$c60.test(input.charAt(peg$currPos))) { s0 = input.charAt(peg$currPos); peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c51); } + if (peg$silentFails === 0) { peg$fail(peg$c61); } } return s0; } + + function simpleLocation (location) { + // Returns an object representing the position of the function within the expression, + // demarcated by the position of its first character and last character. We calculate these values + // using the offset because the expression could span multiple lines, and we don't want to deal + // with column and line values. + return { + min: location.start.offset, + max: location.end.offset + } + } + + peg$result = peg$startRuleFunction(); if (peg$result !== peg$FAILED && peg$currPos === input.length) { diff --git a/packages/kbn-tinymath/src/grammar.pegjs b/packages/kbn-tinymath/src/grammar.pegjs index cab8e024e60b3..9cb92fa9374a2 100644 --- a/packages/kbn-tinymath/src/grammar.pegjs +++ b/packages/kbn-tinymath/src/grammar.pegjs @@ -1,5 +1,18 @@ // tinymath parsing grammar +{ + function simpleLocation (location) { + // Returns an object representing the position of the function within the expression, + // demarcated by the position of its first character and last character. We calculate these values + // using the offset because the expression could span multiple lines, and we don't want to deal + // with column and line values. + return { + min: location.start.offset, + max: location.end.offset + } + } +} + start = Expression @@ -23,18 +36,28 @@ ValidChar // literals and variables Literal "literal" - = _ literal:(Number / VariableWithQuote / Variable) _ { + = _ literal:(Number / Variable) _ { return literal; } +// Quoted variables are interpreted as strings +// but unquoted variables are more restrictive Variable - = _ first:StartChar rest:ValidChar* _ { // We can open this up later. Strict for now. - return first + rest.join(''); + = _ Quote chars:(ValidChar / Space)* Quote _ { + return { + type: 'variable', + value: chars.join(''), + location: simpleLocation(location()), + text: text() + }; } - -VariableWithQuote - = _ Quote first:StartChar mid:(Space* ValidChar+)* Quote _ { - return first + mid.map(m => m[0].join('') + m[1].join('')).join('') + / _ rest:ValidChar+ _ { + return { + type: 'variable', + value: rest.join(''), + location: simpleLocation(location()), + text: text() + }; } // expressions @@ -45,16 +68,22 @@ Expression AddSubtract = _ left:MultiplyDivide rest:(('+' / '-') MultiplyDivide)* _ { return rest.reduce((acc, curr) => ({ + type: 'function', name: curr[0] === '+' ? 'add' : 'subtract', - args: [acc, curr[1]] + args: [acc, curr[1]], + location: simpleLocation(location()), + text: text() }), left) } MultiplyDivide = _ left:Factor rest:(('*' / '/') Factor)* _ { return rest.reduce((acc, curr) => ({ + type: 'function', name: curr[0] === '*' ? 'multiply' : 'divide', - args: [acc, curr[1]] + args: [acc, curr[1]], + location: simpleLocation(location()), + text: text() }), left) } @@ -68,20 +97,46 @@ Group return expr } -Arguments "arguments" - = _ first:Expression rest:(_ ',' _ arg:Expression {return arg})* _ ','? _ { +Argument_List "arguments" + = first:Argument rest:(_ ',' _ arg:Argument {return arg})* _ ','? { return [first].concat(rest); } +String + = [\"] value:(ValidChar)+ [\"] { return value.join(''); } + / [\'] value:(ValidChar)+ [\'] { return value.join(''); } + / value:(ValidChar)+ { return value.join(''); } + + +Argument + = name:[a-zA-Z_]+ _ '=' _ value:(Number / String) _ { + return { + type: 'namedArgument', + name: name.join(''), + value: value, + location: simpleLocation(location()), + text: text() + }; + } + / arg:Expression + Function "function" - = _ name:[a-z]+ '(' _ args:Arguments? _ ')' _ { - return {name: name.join(''), args: args || []}; + = _ name:[a-zA-Z_-]+ '(' _ args:Argument_List? _ ')' _ { + return { + type: 'function', + name: name.join(''), + args: args || [], + location: simpleLocation(location()), + text: text() + }; } // Numbers. Lol. Number "number" - = '-'? Integer Fraction? Exp? { return parseFloat(text()); } + = '-'? Integer Fraction? Exp? { + return parseFloat(text()); + } E = [eE] diff --git a/packages/kbn-tinymath/src/index.js b/packages/kbn-tinymath/src/index.js index fd4d167bf04dc..4db7df9c57315 100644 --- a/packages/kbn-tinymath/src/index.js +++ b/packages/kbn-tinymath/src/index.js @@ -38,17 +38,22 @@ function interpret(node, scope, injectedFunctions) { return exec(node); function exec(node) { - const type = getType(node); + if (typeof node === 'number') { + return node; + } - if (type === 'function') return invoke(node); + if (node.type === 'function') return invoke(node); - if (type === 'string') { - const val = getValue(scope, node); - if (typeof val === 'undefined') throw new Error(`Unknown variable: ${node}`); + if (node.type === 'variable') { + const val = getValue(scope, node.value); + if (typeof val === 'undefined') throw new Error(`Unknown variable: ${node.value}`); return val; } - return node; // Can only be a number at this point + if (node.type === 'namedArgument') { + // We are ignoring named arguments in the interpreter + throw new Error(`Named arguments are not supported in tinymath itself, at ${node.name}`); + } } function invoke(node) { @@ -67,17 +72,6 @@ function getValue(scope, node) { return typeof val !== 'undefined' ? val : scope[node]; } -function getType(x) { - const type = typeof x; - if (type === 'object') { - const keys = Object.keys(x); - if (keys.length !== 2 || !x.name || !x.args) throw new Error('Invalid AST object'); - return 'function'; - } - if (type === 'string' || type === 'number') return type; - throw new Error(`Unknown AST property type: ${type}`); -} - function isOperable(args) { return args.every((arg) => { if (Array.isArray(arg)) return isOperable(arg); diff --git a/packages/kbn-tinymath/test/library.test.js b/packages/kbn-tinymath/test/library.test.js index 01b4aa3fbf7ae..d11822625b98f 100644 --- a/packages/kbn-tinymath/test/library.test.js +++ b/packages/kbn-tinymath/test/library.test.js @@ -11,7 +11,19 @@ Need tests for spacing, etc */ -const { evaluate, parse } = require('..'); +import { evaluate, parse } from '..'; + +function variableEqual(value) { + return expect.objectContaining({ type: 'variable', value }); +} + +function functionEqual(name, args) { + return expect.objectContaining({ type: 'function', name, args }); +} + +function namedArgumentEqual(name, value) { + return expect.objectContaining({ type: 'namedArgument', name, value }); +} describe('Parser', () => { describe('Numbers', () => { @@ -31,96 +43,144 @@ describe('Parser', () => { describe('Variables', () => { it('strings', () => { - expect(parse('f')).toEqual('f'); - expect(parse('foo')).toEqual('foo'); + expect(parse('f')).toEqual(variableEqual('f')); + expect(parse('foo')).toEqual(variableEqual('foo')); + expect(parse('foo1')).toEqual(variableEqual('foo1')); + expect(() => parse('1foo1')).toThrow('but "f" found'); + }); + + it('strings with spaces', () => { + expect(parse(' foo ')).toEqual(variableEqual('foo')); + expect(() => parse(' foo bar ')).toThrow('but "b" found'); }); it('allowed characters', () => { - expect(parse('_foo')).toEqual('_foo'); - expect(parse('@foo')).toEqual('@foo'); - expect(parse('.foo')).toEqual('.foo'); - expect(parse('-foo')).toEqual('-foo'); - expect(parse('_foo0')).toEqual('_foo0'); - expect(parse('@foo0')).toEqual('@foo0'); - expect(parse('.foo0')).toEqual('.foo0'); - expect(parse('-foo0')).toEqual('-foo0'); + expect(parse('_foo')).toEqual(variableEqual('_foo')); + expect(parse('@foo')).toEqual(variableEqual('@foo')); + expect(parse('.foo')).toEqual(variableEqual('.foo')); + expect(parse('-foo')).toEqual(variableEqual('-foo')); + expect(parse('_foo0')).toEqual(variableEqual('_foo0')); + expect(parse('@foo0')).toEqual(variableEqual('@foo0')); + expect(parse('.foo0')).toEqual(variableEqual('.foo0')); + expect(parse('-foo0')).toEqual(variableEqual('-foo0')); }); }); describe('quoted variables', () => { it('strings with double quotes', () => { - expect(parse('"foo"')).toEqual('foo'); - expect(parse('"f b"')).toEqual('f b'); - expect(parse('"foo bar"')).toEqual('foo bar'); - expect(parse('"foo bar fizz buzz"')).toEqual('foo bar fizz buzz'); - expect(parse('"foo bar baby"')).toEqual('foo bar baby'); + expect(parse('"foo"')).toEqual(variableEqual('foo')); + expect(parse('"f b"')).toEqual(variableEqual('f b')); + expect(parse('"foo bar"')).toEqual(variableEqual('foo bar')); + expect(parse('"foo bar fizz buzz"')).toEqual(variableEqual('foo bar fizz buzz')); + expect(parse('"foo bar baby"')).toEqual(variableEqual('foo bar baby')); }); it('strings with single quotes', () => { /* eslint-disable prettier/prettier */ - expect(parse("'foo'")).toEqual('foo'); - expect(parse("'f b'")).toEqual('f b'); - expect(parse("'foo bar'")).toEqual('foo bar'); - expect(parse("'foo bar fizz buzz'")).toEqual('foo bar fizz buzz'); - expect(parse("'foo bar baby'")).toEqual('foo bar baby'); + expect(parse("'foo'")).toEqual(variableEqual('foo')); + expect(parse("'f b'")).toEqual(variableEqual('f b')); + expect(parse("'foo bar'")).toEqual(variableEqual('foo bar')); + expect(parse("'foo bar fizz buzz'")).toEqual(variableEqual('foo bar fizz buzz')); + expect(parse("'foo bar baby'")).toEqual(variableEqual('foo bar baby')); + expect(parse("' foo bar'")).toEqual(variableEqual(" foo bar")); + expect(parse("'foo bar '")).toEqual(variableEqual("foo bar ")); + expect(parse("'0foo'")).toEqual(variableEqual("0foo")); + expect(parse("' foo bar'")).toEqual(variableEqual(" foo bar")); + expect(parse("'foo bar '")).toEqual(variableEqual("foo bar ")); + expect(parse("'0foo'")).toEqual(variableEqual("0foo")); /* eslint-enable prettier/prettier */ }); it('allowed characters', () => { - expect(parse('"_foo bar"')).toEqual('_foo bar'); - expect(parse('"@foo bar"')).toEqual('@foo bar'); - expect(parse('".foo bar"')).toEqual('.foo bar'); - expect(parse('"-foo bar"')).toEqual('-foo bar'); - expect(parse('"_foo0 bar1"')).toEqual('_foo0 bar1'); - expect(parse('"@foo0 bar1"')).toEqual('@foo0 bar1'); - expect(parse('".foo0 bar1"')).toEqual('.foo0 bar1'); - expect(parse('"-foo0 bar1"')).toEqual('-foo0 bar1'); - }); - - it('invalid characters in double quotes', () => { - const check = (str) => () => parse(str); - expect(check('" foo bar"')).toThrow('but "\\"" found'); - expect(check('"foo bar "')).toThrow('but "\\"" found'); - expect(check('"0foo"')).toThrow('but "\\"" found'); - expect(check('" foo bar"')).toThrow('but "\\"" found'); - expect(check('"foo bar "')).toThrow('but "\\"" found'); - expect(check('"0foo"')).toThrow('but "\\"" found'); - }); - - it('invalid characters in single quotes', () => { - const check = (str) => () => parse(str); - /* eslint-disable prettier/prettier */ - expect(check("' foo bar'")).toThrow('but "\'" found'); - expect(check("'foo bar '")).toThrow('but "\'" found'); - expect(check("'0foo'")).toThrow('but "\'" found'); - expect(check("' foo bar'")).toThrow('but "\'" found'); - expect(check("'foo bar '")).toThrow('but "\'" found'); - expect(check("'0foo'")).toThrow('but "\'" found'); - /* eslint-enable prettier/prettier */ + expect(parse('"_foo bar"')).toEqual(variableEqual('_foo bar')); + expect(parse('"@foo bar"')).toEqual(variableEqual('@foo bar')); + expect(parse('".foo bar"')).toEqual(variableEqual('.foo bar')); + expect(parse('"-foo bar"')).toEqual(variableEqual('-foo bar')); + expect(parse('"_foo0 bar1"')).toEqual(variableEqual('_foo0 bar1')); + expect(parse('"@foo0 bar1"')).toEqual(variableEqual('@foo0 bar1')); + expect(parse('".foo0 bar1"')).toEqual(variableEqual('.foo0 bar1')); + expect(parse('"-foo0 bar1"')).toEqual(variableEqual('-foo0 bar1')); + expect(parse('" foo bar"')).toEqual(variableEqual(' foo bar')); + expect(parse('"foo bar "')).toEqual(variableEqual('foo bar ')); + expect(parse('"0foo"')).toEqual(variableEqual('0foo')); + expect(parse('" foo bar"')).toEqual(variableEqual(' foo bar')); + expect(parse('"foo bar "')).toEqual(variableEqual('foo bar ')); + expect(parse('"0foo"')).toEqual(variableEqual('0foo')); }); }); describe('Functions', () => { it('no arguments', () => { - expect(parse('foo()')).toEqual({ name: 'foo', args: [] }); + expect(parse('foo()')).toEqual(functionEqual('foo', [])); }); it('arguments', () => { - expect(parse('foo(5,10)')).toEqual({ name: 'foo', args: [5, 10] }); + expect(parse('foo(5,10)')).toEqual(functionEqual('foo', [5, 10])); }); it('arguments with strings', () => { - expect(parse('foo("string with spaces")')).toEqual({ - name: 'foo', - args: ['string with spaces'], - }); + expect(parse('foo("string with spaces")')).toEqual( + functionEqual('foo', [variableEqual('string with spaces')]) + ); - /* eslint-disable prettier/prettier */ - expect(parse("foo('string with spaces')")).toEqual({ - name: 'foo', - args: ['string with spaces'], - }); - /* eslint-enable prettier/prettier */ + expect(parse("foo('string with spaces')")).toEqual( + functionEqual('foo', [variableEqual('string with spaces')]) + ); + }); + + it('named only', () => { + expect(parse('foo(q=10)')).toEqual(functionEqual('foo', [namedArgumentEqual('q', 10)])); + }); + + it('named argument is numeric', () => { + expect(parse('foo(q=10.1234e5)')).toEqual( + functionEqual('foo', [namedArgumentEqual('q', 10.1234e5)]) + ); + }); + + it('named and positional', () => { + expect(parse('foo(ref, q="bar")')).toEqual( + functionEqual('foo', [variableEqual('ref'), namedArgumentEqual('q', 'bar')]) + ); + }); + + it('numerically named', () => { + expect(() => parse('foo(1=2)')).toThrow('but "(" found'); + }); + + it('multiple named', () => { + expect(parse('foo(q_param="bar", offset="1d")')).toEqual( + functionEqual('foo', [ + namedArgumentEqual('q_param', 'bar'), + namedArgumentEqual('offset', '1d'), + ]) + ); + }); + + it('multiple named and positional', () => { + expect(parse('foo(q="bar", ref, offset="1d", 100)')).toEqual( + functionEqual('foo', [ + namedArgumentEqual('q', 'bar'), + variableEqual('ref'), + namedArgumentEqual('offset', '1d'), + 100, + ]) + ); + }); + + it('duplicate named', () => { + expect(parse('foo(q="bar", q="test")')).toEqual( + functionEqual('foo', [namedArgumentEqual('q', 'bar'), namedArgumentEqual('q', 'test')]) + ); + }); + + it('incomplete named', () => { + expect(() => parse('foo(a=)')).toThrow('but "(" found'); + expect(() => parse('foo(=a)')).toThrow('but "(" found'); + }); + + it('invalid named', () => { + expect(() => parse('foo(offset-type="1d")')).toThrow('but "(" found'); }); }); @@ -155,7 +215,7 @@ describe('Evaluate', () => { ); }); - it('valiables with dots', () => { + it('variables with dots', () => { expect(evaluate('foo.bar', { 'foo.bar': 20 })).toEqual(20); expect(evaluate('"is.null"', { 'is.null': null })).toEqual(null); expect(evaluate('"is.false"', { 'is.null': null, 'is.false': false })).toEqual(false); @@ -210,6 +270,10 @@ describe('Evaluate', () => { expect(evaluate('sum("space name")', { 'space name': [1, 2, 21] })).toEqual(24); }); + it('throws on named arguments', () => { + expect(() => evaluate('sum(invalid=a)')).toThrow('Named arguments are not supported'); + }); + it('equations with injected functions', () => { expect( evaluate( diff --git a/packages/kbn-tinymath/tinymath.d.ts b/packages/kbn-tinymath/tinymath.d.ts new file mode 100644 index 0000000000000..c3c32a59fa15a --- /dev/null +++ b/packages/kbn-tinymath/tinymath.d.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export function parse(expression: string): TinymathAST; +export function evaluate( + expression: string | null, + context: Record +): number | number[]; + +// Named arguments are not top-level parts of the grammar, but can be nested +export type TinymathAST = number | TinymathVariable | TinymathFunction | TinymathNamedArgument; + +// Zero-indexed location +export interface TinymathLocation { + min: number; + max: number; +} + +export interface TinymathFunction { + type: 'function'; + name: string; + text: string; + args: TinymathAST[]; + location: TinymathLocation; +} + +export interface TinymathVariable { + type: 'variable'; + value: string; + text: string; + location: TinymathLocation; +} + +export interface TinymathNamedArgument { + type: 'namedArgument'; + name: string; + value: string; + text: string; + location: TinymathLocation; +} diff --git a/packages/kbn-tinymath/tsconfig.json b/packages/kbn-tinymath/tsconfig.json new file mode 100644 index 0000000000000..62a7376efdfa6 --- /dev/null +++ b/packages/kbn-tinymath/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-tinymath" + }, + "include": ["tinymath.d.ts"] +} diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js index 5376abbc1a088..1e30720d6e5b2 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js @@ -134,9 +134,7 @@ describe('math(resp, panel, series)', () => { series )(await mathAgg(resp, panel, series)((results) => results))([]); } catch (e) { - expect(e.message).toEqual( - 'Failed to parse expression. Expected "*", "+", "-", "/", or end of input but "(" found.' - ); + expect(e.message).toEqual('No such function: notExistingFn'); } }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts index 15ca11c902280..af70fa729b7da 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts @@ -5,7 +5,6 @@ * 2.0. */ -// @ts-expect-error no @typed def; Elastic library import { evaluate } from '@kbn/tinymath'; import { pivotObjectArray } from '../../../common/lib/pivot_object_array'; import { Datatable, isDatatable, ExpressionFunctionDefinition } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.ts similarity index 96% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.ts index 0345f05efa8ff..81b7517686b1c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.ts @@ -11,7 +11,7 @@ import { getExpressionType } from './pointseries/lib/get_expression_type'; describe('getExpressionType', () => { it('returns the result type of an evaluated math expression', () => { expect(getExpressionType(testTable.columns, '2')).toBe('number'); - expect(getExpressionType(testTable.colunns, '2 + 3')).toBe('number'); + expect(getExpressionType(testTable.columns, '2 + 3')).toBe('number'); expect(getExpressionType(testTable.columns, 'name')).toBe('string'); expect(getExpressionType(testTable.columns, 'time')).toBe('date'); expect(getExpressionType(testTable.columns, 'price')).toBe('number'); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts index 136fbf2ac5d13..dc2b85c7393b4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -// @ts-expect-error untyped library import { parse } from '@kbn/tinymath'; import { getFieldNames } from './pointseries/lib/get_field_names'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts index a88a31388eeeb..38438ffb4ad66 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -// @ts-expect-error Untyped Elastic library import { evaluate } from '@kbn/tinymath'; import { groupBy, zipObject, omit, uniqBy } from 'lodash'; import moment from 'moment'; @@ -20,7 +19,6 @@ import { import { pivotObjectArray } from '../../../../common/lib/pivot_object_array'; import { unquoteString } from '../../../../common/lib/unquote_string'; import { isColumnReference } from './lib/is_column_reference'; -// @ts-expect-error untyped local import { getExpressionType } from './lib/get_expression_type'; import { getFunctionHelp, getFunctionErrors } from '../../../../i18n'; @@ -132,16 +130,17 @@ export function pointseries(): ExpressionFunctionDefinition< [PRIMARY_KEY]: i, })); - function normalizeValue(expression: string, value: string) { + function normalizeValue(expression: string, value: number | number[], index: number) { + const numberValue = Array.isArray(value) ? value[index] : value; switch (getExpressionType(input.columns, expression)) { case 'string': - return String(value); + return String(numberValue); case 'number': - return Number(value); + return Number(numberValue); case 'date': - return moment(value).valueOf(); + return moment(numberValue).valueOf(); default: - return value; + return numberValue; } } @@ -153,7 +152,7 @@ export function pointseries(): ExpressionFunctionDefinition< (acc: Record, { name, value }) => { try { acc[name] = args[name] - ? normalizeValue(value, evaluate(value, mathScope)[i]) + ? normalizeValue(value, evaluate(value, mathScope), i) : '_all'; } catch (e) { // TODO: handle invalid column names... diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.ts similarity index 82% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.ts index 5ed10a084e34f..80ac627747318 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.ts @@ -6,11 +6,12 @@ */ import { parse } from '@kbn/tinymath'; +import { DatatableColumn } from 'src/plugins/expressions/common'; import { getFieldType } from '../../../../../common/lib/get_field_type'; import { isColumnReference } from './is_column_reference'; import { getFieldNames } from './get_field_names'; -export function getExpressionType(columns, mathExpression) { +export function getExpressionType(columns: DatatableColumn[], mathExpression: string) { // if isColumnReference returns true, then mathExpression is just a string // referencing a column in a datatable if (isColumnReference(mathExpression)) { @@ -19,7 +20,7 @@ export function getExpressionType(columns, mathExpression) { const parsedMath = parse(mathExpression); - if (parsedMath.args) { + if (typeof parsedMath !== 'number' && parsedMath.type === 'function') { const fieldNames = parsedMath.args.reduce(getFieldNames, []); if (fieldNames.length > 0) { @@ -30,7 +31,7 @@ export function getExpressionType(columns, mathExpression) { } return types; - }, []); + }, [] as string[]); return fieldTypes.length === 1 ? fieldTypes[0] : 'string'; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_field_names.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_field_names.ts index 5ae27b27c66f2..550705fdddd7f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_field_names.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_field_names.ts @@ -5,21 +5,19 @@ * 2.0. */ -type Arg = - | string - | number - | { - name: string; - args: Arg[]; - }; +import { TinymathAST } from '@kbn/tinymath'; -export function getFieldNames(names: string[], arg: Arg): string[] { - if (typeof arg === 'object' && arg.args !== undefined) { - return names.concat(arg.args.reduce(getFieldNames, [])); +export function getFieldNames(names: string[], ast: TinymathAST): string[] { + if (typeof ast === 'number') { + return names; } - if (typeof arg === 'string') { - return names.concat(arg); + if (ast.type === 'function') { + return names.concat(ast.args.reduce(getFieldNames, [])); + } + + if (ast.type === 'variable') { + return names.concat(ast.value); } return names; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts index abcd953a4e123..4b9de8b90cb20 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts @@ -5,7 +5,6 @@ * 2.0. */ -// @ts-expect-error untyped library import { parse } from '@kbn/tinymath'; export function isColumnReference(mathExpression: string | null): boolean { @@ -13,5 +12,5 @@ export function isColumnReference(mathExpression: string | null): boolean { mathExpression = 'null'; } const parsedMath = parse(mathExpression); - return typeof parsedMath === 'string'; + return typeof parsedMath !== 'number' && parsedMath.type === 'variable'; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.js rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.ts similarity index 71% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.js rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.ts index 7e7930f39c9bd..015dca39402b5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.ts @@ -9,7 +9,7 @@ import { parse } from '@kbn/tinymath'; import { unquoteString } from '../../../../common/lib/unquote_string'; // break out into separate function, write unit tests first -export function getFormObject(argValue) { +export function getFormObject(argValue: string) { if (argValue === '') { return { fn: '', @@ -20,23 +20,28 @@ export function getFormObject(argValue) { // check if the value is a math expression, and set its type if it is const mathObj = parse(argValue); // A symbol node is a plain string, so we guess that they're looking for a column. - if (typeof mathObj === 'string') { + if (typeof mathObj === 'number') { + throw new Error(`Cannot render scalar values or complex math expressions`); + } + + if (mathObj.type === 'variable') { return { fn: '', - column: unquoteString(argValue), + column: unquoteString(mathObj.value), }; } // Check if its a simple function, eg a function wrapping a symbol node // check for only one arg of type string if ( - typeof mathObj === 'object' && + mathObj.type === 'function' && mathObj.args.length === 1 && - typeof mathObj.args[0] === 'string' + typeof mathObj.args[0] !== 'number' && + mathObj.args[0].type === 'variable' ) { return { fn: mathObj.name, - column: unquoteString(mathObj.args[0]), + column: unquoteString(mathObj.args[0].value), }; } From 3ad86f13281cb7bbca55123faddae43b15263375 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 9 Feb 2021 15:50:00 +0000 Subject: [PATCH 062/100] [Logs UI] Add setup error telemetry for ML functionality (#90298) * Add logs ML setup error telemetry --- .../process_step/process_step.tsx | 2 +- .../log_analysis/api/ml_setup_module_api.ts | 13 ++++++++- .../logs/log_analysis/log_analysis_module.tsx | 27 ++++++++++++++++++- .../log_analysis_module_status.tsx | 4 +-- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx index ed26bd5b2077c..987ae87423fda 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx @@ -75,7 +75,7 @@ export const ProcessStep: React.FunctionComponent = ({ defaultMessage="Something went wrong creating the necessary ML jobs. Please ensure all selected log indices exist." /> - {errorMessages.map((errorMessage, i) => ( + {setupStatus.reasons.map((errorMessage, i) => ( {errorMessage} diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts index 69846e1f51482..ea1567d6056f1 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts @@ -91,8 +91,19 @@ const setupMlModuleRequestPayloadRT = rt.intersection([ setupMlModuleRequestParamsRT, ]); +const setupErrorRT = rt.type({ + reason: rt.string, + type: rt.string, +}); + const setupErrorResponseRT = rt.type({ - msg: rt.string, + status: rt.number, + error: rt.intersection([ + setupErrorRT, + rt.type({ + root_cause: rt.array(setupErrorRT), + }), + ]), }); const datafeedSetupResponseRT = rt.intersection([ diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx index 72b74d5f99719..00a6c3c2a72fb 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx @@ -11,6 +11,7 @@ import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { useModuleStatus } from './log_analysis_module_status'; import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; +import { useUiTracker } from '../../../../../observability/public'; export const useLogAnalysisModule = ({ sourceConfiguration, @@ -23,6 +24,8 @@ export const useLogAnalysisModule = ({ const { spaceId, sourceId, timestampField } = sourceConfiguration; const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes); + const trackMetric = useUiTracker({ app: 'infra_logs' }); + const [, fetchJobStatus] = useTrackedPromise( { cancelPreviousOn: 'resolution', @@ -75,6 +78,25 @@ export const useLogAnalysisModule = ({ return { setupResult, jobSummaries }; }, onResolve: ({ setupResult: { datafeeds, jobs }, jobSummaries }) => { + // Track failures + if ( + [...datafeeds, ...jobs] + .reduce((acc, resource) => [...acc, ...Object.keys(resource)], []) + .some((key) => key === 'error') + ) { + const reasons = [...datafeeds, ...jobs] + .filter((resource) => resource.error !== undefined) + .map((resource) => resource.error?.error?.reason ?? ''); + // NOTE: Lack of indices and a missing field mapping have the same error + if ( + reasons.filter((reason) => reason.includes('because it has no mappings')).length > 0 + ) { + trackMetric({ metric: 'logs_ml_setup_error_bad_indices_or_mappings' }); + } else { + trackMetric({ metric: 'logs_ml_setup_error_unknown_cause' }); + } + } + dispatchModuleStatus({ type: 'finishedSetup', datafeedSetupResults: datafeeds, @@ -84,8 +106,11 @@ export const useLogAnalysisModule = ({ sourceId, }); }, - onReject: () => { + onReject: (e: any) => { dispatchModuleStatus({ type: 'failedSetup' }); + if (e?.body?.statusCode === 403) { + trackMetric({ metric: 'logs_ml_setup_error_lack_of_privileges' }); + } }, }, [moduleDescriptor.setUpModule, spaceId, sourceId, timestampField] diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx index 1fec67228aa22..c3117c9326d1e 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx @@ -105,10 +105,10 @@ const createStatusReducer = (jobTypes: JobType[]) => ( reasons: [ ...Object.values(datafeedSetupResults) .filter(hasError) - .map((datafeed) => datafeed.error.msg), + .map((datafeed) => datafeed.error.error?.reason), ...Object.values(jobSetupResults) .filter(hasError) - .map((job) => job.error.msg), + .map((job) => job.error.error?.reason), ], }; From d43547af82426f113c29867a7716fe895c318324 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Feb 2021 07:53:00 -0800 Subject: [PATCH 063/100] skip flaky suite (#86546) --- .../functional/apps/upgrade_assistant/upgrade_assistant.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts index 711c9b7683678..93955fb741044 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts @@ -18,7 +18,8 @@ export default function upgradeAssistantFunctionalTests({ const log = getService('log'); const retry = getService('retry'); - describe('Upgrade Checkup', function () { + // Failing: See https://github.com/elastic/kibana/issues/86546 + describe.skip('Upgrade Checkup', function () { this.tags('includeFirefox'); before(async () => { From 4b3d1bf83c07d0cc1618b26729b8008778a97cb0 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 9 Feb 2021 10:01:40 -0700 Subject: [PATCH 064/100] [Tabify] Add meta option to include top-level underscored field values (#90535) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/tabify_docs.test.ts.snap | 284 ++++++++++++++++++ .../common/search/tabify/tabify_docs.test.ts | 9 + .../data/common/search/tabify/tabify_docs.ts | 17 +- 3 files changed, 305 insertions(+), 5 deletions(-) diff --git a/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap index 6cc191a67633c..22276335a0599 100644 --- a/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap +++ b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap @@ -1,5 +1,113 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`tabifyDocs combines meta fields if meta option is set 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested", + "meta": Object { + "field": "nested", + "index": "test-index", + "params": undefined, + "type": "object", + }, + "name": "nested", + }, + Object { + "id": "sourceTest", + "meta": Object { + "field": "sourceTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "sourceTest", + }, + Object { + "id": "_id", + "meta": Object { + "field": "_id", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_id", + }, + Object { + "id": "_index", + "meta": Object { + "field": "_index", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_index", + }, + Object { + "id": "_score", + "meta": Object { + "field": "_score", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "_score", + }, + Object { + "id": "_type", + "meta": Object { + "field": "_type", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_type", + }, + ], + "rows": Array [ + Object { + "_id": "hit-id-value", + "_index": "hit-index-value", + "_score": 77, + "_type": "hit-type-value", + "fieldTest": 123, + "invalidMapping": 345, + "nested": Array [ + Object { + "field": 123, + }, + ], + "sourceTest": 123, + }, + ], + "type": "datatable", +} +`; + exports[`tabifyDocs converts fields by default 1`] = ` Object { "columns": Array [ @@ -47,9 +155,53 @@ Object { }, "name": "sourceTest", }, + Object { + "id": "_id", + "meta": Object { + "field": "_id", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_id", + }, + Object { + "id": "_index", + "meta": Object { + "field": "_index", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_index", + }, + Object { + "id": "_score", + "meta": Object { + "field": "_score", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "_score", + }, + Object { + "id": "_type", + "meta": Object { + "field": "_type", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_type", + }, ], "rows": Array [ Object { + "_id": "hit-id-value", + "_index": "hit-index-value", + "_score": 77, + "_type": "hit-type-value", "fieldTest": 123, "invalidMapping": 345, "nested": Array [ @@ -111,9 +263,53 @@ Object { }, "name": "sourceTest", }, + Object { + "id": "_id", + "meta": Object { + "field": "_id", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_id", + }, + Object { + "id": "_index", + "meta": Object { + "field": "_index", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_index", + }, + Object { + "id": "_score", + "meta": Object { + "field": "_score", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "_score", + }, + Object { + "id": "_type", + "meta": Object { + "field": "_type", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_type", + }, ], "rows": Array [ Object { + "_id": "hit-id-value", + "_index": "hit-index-value", + "_score": 77, + "_type": "hit-type-value", "fieldTest": 123, "invalidMapping": 345, "nested": Array [ @@ -175,9 +371,53 @@ Object { }, "name": "sourceTest", }, + Object { + "id": "_id", + "meta": Object { + "field": "_id", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_id", + }, + Object { + "id": "_index", + "meta": Object { + "field": "_index", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_index", + }, + Object { + "id": "_score", + "meta": Object { + "field": "_score", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "_score", + }, + Object { + "id": "_type", + "meta": Object { + "field": "_type", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_type", + }, ], "rows": Array [ Object { + "_id": "hit-id-value", + "_index": "hit-index-value", + "_score": 77, + "_type": "hit-type-value", "fieldTest": 123, "invalidMapping": 345, "nested": Array [ @@ -235,9 +475,53 @@ Object { }, "name": "sourceTest", }, + Object { + "id": "_id", + "meta": Object { + "field": "_id", + "index": undefined, + "params": undefined, + "type": "string", + }, + "name": "_id", + }, + Object { + "id": "_index", + "meta": Object { + "field": "_index", + "index": undefined, + "params": undefined, + "type": "string", + }, + "name": "_index", + }, + Object { + "id": "_score", + "meta": Object { + "field": "_score", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "_score", + }, + Object { + "id": "_type", + "meta": Object { + "field": "_type", + "index": undefined, + "params": undefined, + "type": "string", + }, + "name": "_type", + }, ], "rows": Array [ Object { + "_id": "hit-id-value", + "_index": "hit-index-value", + "_score": 77, + "_type": "hit-type-value", "fieldTest": 123, "invalidMapping": 345, "nested": Array [ diff --git a/src/plugins/data/common/search/tabify/tabify_docs.test.ts b/src/plugins/data/common/search/tabify/tabify_docs.test.ts index c81e39f4c156a..52e12aeee1ae6 100644 --- a/src/plugins/data/common/search/tabify/tabify_docs.test.ts +++ b/src/plugins/data/common/search/tabify/tabify_docs.test.ts @@ -37,6 +37,10 @@ describe('tabifyDocs', () => { hits: { hits: [ { + _id: 'hit-id-value', + _index: 'hit-index-value', + _type: 'hit-type-value', + _score: 77, _source: { sourceTest: 123 }, fields: { fieldTest: 123, invalidMapping: 345, nested: [{ field: 123 }] }, }, @@ -59,6 +63,11 @@ describe('tabifyDocs', () => { expect(table).toMatchSnapshot(); }); + it('combines meta fields if meta option is set', () => { + const table = tabifyDocs(response, index, { meta: true }); + expect(table).toMatchSnapshot(); + }); + it('works without provided index pattern', () => { const table = tabifyDocs(response); expect(table).toMatchSnapshot(); diff --git a/src/plugins/data/common/search/tabify/tabify_docs.ts b/src/plugins/data/common/search/tabify/tabify_docs.ts index eaf43d9fd6ff6..b4806283e63f2 100644 --- a/src/plugins/data/common/search/tabify/tabify_docs.ts +++ b/src/plugins/data/common/search/tabify/tabify_docs.ts @@ -11,6 +11,12 @@ import { isPlainObject } from 'lodash'; import { IndexPattern } from '../../index_patterns/index_patterns'; import { Datatable, DatatableColumn, DatatableColumnType } from '../../../../expressions/common'; +export interface TabifyDocsOptions { + shallow?: boolean; + source?: boolean; + meta?: boolean; +} + export function flattenHit( hit: SearchResponse['hits']['hits'][0], indexPattern?: IndexPattern, @@ -56,12 +62,13 @@ export function flattenHit( if (params?.source !== false && hit._source) { flatten(hit._source as Record); } - return flat; -} + if (params?.meta !== false) { + // combine the fields that Discover allows to add as columns + const { _id, _index, _type, _score } = hit; + flatten({ _id, _index, _score, _type }); + } -export interface TabifyDocsOptions { - shallow?: boolean; - source?: boolean; + return flat; } export const tabifyDocs = ( From f3399620cf558efe1e3a862e32289839f88ed815 Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 9 Feb 2021 09:33:39 -0800 Subject: [PATCH 065/100] [Enterprise Search] Add eslint import/order rules (#90530) * Add import rules - newlines between each group - mocks in test files before everything else - React before all other externals * Run --fix on public/applications/workplace_search * Manually fix errors still present in WS public files - these appear to be mostly due to jest.mock() or const mixed in with imports, which confuses the autofixer * Run --fix on public/applications/app_search + some manual fixes/tweaks along the way * Run --fix on public/applications/shared - some opinionated changes, particularly around IFlashMessages and grouping together types coming from Kibana (src->../../src) * Run --fix on public/applications/enterprise_search - mostly straightforward * Run --fix on public/applications/__mocks__ * Fix remaining top-level public files - Some opinionated changes (src/core -> ../../src/core) to keep types/mocks together * Run --fix on server/ files - same opinionated src->../src changes to keep deps grouped together - opinionated require->import fetch change in enterprise_esarch_config_api.test.ts - opinionated [] inclusion of builtins & external imports together (mostly for enterprise_search_request_handler.ts) --- .eslintrc.js | 33 +++++++++++++++++++ .../public/applications/__mocks__/kea.mock.ts | 4 +-- .../__mocks__/kibana_logic.mock.ts | 1 + .../__mocks__/mount_async.mock.tsx | 3 +- .../__mocks__/mount_with_i18n.mock.tsx | 2 ++ .../__mocks__/shallow_with_i18n.mock.tsx | 1 + .../applications/app_search/app_logic.test.ts | 2 +- .../applications/app_search/app_logic.ts | 1 + .../analytics/analytics_layout.test.tsx | 5 +-- .../components/analytics/analytics_layout.tsx | 7 ++-- .../analytics/analytics_logic.test.ts | 1 + .../components/analytics/analytics_logic.ts | 4 +-- .../analytics/analytics_router.test.tsx | 4 ++- .../components/analytics_cards.test.tsx | 2 ++ .../analytics/components/analytics_cards.tsx | 1 + .../components/analytics_chart.test.tsx | 2 ++ .../analytics/components/analytics_chart.tsx | 2 ++ .../components/analytics_header.test.tsx | 3 ++ .../analytics/components/analytics_header.tsx | 8 ++--- .../components/analytics_search.test.tsx | 2 ++ .../analytics/components/analytics_search.tsx | 3 +- .../components/analytics_section.test.tsx | 1 + .../analytics_tables/analytics_table.test.tsx | 1 + .../analytics_tables/analytics_table.tsx | 4 ++- .../inline_tags_list.test.tsx | 2 ++ .../analytics_tables/inline_tags_list.tsx | 3 +- .../query_clicks_table.test.tsx | 1 + .../analytics_tables/query_clicks_table.tsx | 5 +-- .../recent_queries_table.test.tsx | 1 + .../analytics_tables/recent_queries_table.tsx | 3 +- .../analytics_tables/shared_columns.tsx | 5 +-- .../components/analytics_unavailable.test.tsx | 2 ++ .../components/analytics_unavailable.tsx | 3 +- .../components/analytics/constants.ts | 1 + .../app_search/components/analytics/utils.ts | 5 +-- .../analytics/views/analytics.test.tsx | 2 ++ .../components/analytics/views/analytics.tsx | 9 ++--- .../analytics/views/query_detail.test.tsx | 2 ++ .../analytics/views/query_detail.tsx | 5 +-- .../analytics/views/recent_queries.test.tsx | 2 ++ .../analytics/views/recent_queries.tsx | 5 +-- .../analytics/views/top_queries.test.tsx | 2 ++ .../analytics/views/top_queries.tsx | 5 +-- .../views/top_queries_no_clicks.test.tsx | 2 ++ .../analytics/views/top_queries_no_clicks.tsx | 5 +-- .../views/top_queries_no_results.test.tsx | 2 ++ .../views/top_queries_no_results.tsx | 5 +-- .../views/top_queries_with_clicks.test.tsx | 2 ++ .../views/top_queries_with_clicks.tsx | 5 +-- .../components/credentials/constants.ts | 1 + .../credentials/credentials.test.tsx | 5 ++- .../components/credentials/credentials.tsx | 9 ++--- .../credentials_flyout/body.test.tsx | 4 ++- .../credentials/credentials_flyout/body.tsx | 4 ++- .../credentials_flyout/footer.test.tsx | 2 ++ .../credentials/credentials_flyout/footer.tsx | 2 ++ .../key_engine_access.test.tsx | 2 ++ .../form_components/key_engine_access.tsx | 2 ++ .../form_components/key_name.test.tsx | 2 ++ .../form_components/key_name.tsx | 2 ++ .../key_read_write_access.test.tsx | 2 ++ .../form_components/key_read_write_access.tsx | 2 ++ .../form_components/key_type.test.tsx | 3 ++ .../form_components/key_type.tsx | 4 ++- .../key_update_warning.test.tsx | 2 ++ .../form_components/key_update_warning.tsx | 1 + .../credentials_flyout/header.test.tsx | 2 ++ .../credentials/credentials_flyout/header.tsx | 4 ++- .../credentials_flyout/index.test.tsx | 2 ++ .../credentials/credentials_flyout/index.tsx | 7 ++-- .../credentials_list.test.tsx | 9 +++-- .../credentials_list/credentials_list.tsx | 14 ++++---- .../credentials/credentials_list/key.test.tsx | 2 ++ .../credentials/credentials_list/key.tsx | 1 + .../credentials/credentials_logic.test.ts | 1 + .../credentials/credentials_logic.ts | 12 +++---- .../components/credentials/types.ts | 1 + .../credentials/utils/api_token_sort.test.ts | 3 +- .../utils/get_engines_display_text.test.tsx | 22 +++++++------ .../utils/get_engines_display_text.tsx | 1 + .../api_code_example.test.tsx | 2 ++ .../api_code_example.tsx | 13 ++++---- .../paste_json_text.test.tsx | 3 ++ .../paste_json_text.tsx | 5 +-- .../show_creation_modes.test.tsx | 3 ++ .../show_creation_modes.tsx | 5 +-- .../upload_json_file.test.tsx | 3 ++ .../upload_json_file.tsx | 5 +-- .../errors.test.tsx | 2 ++ .../creation_response_components/errors.tsx | 3 +- .../summary.test.tsx | 6 +++- .../creation_response_components/summary.tsx | 5 +-- .../summary_documents.test.tsx | 2 ++ .../summary_documents.tsx | 2 +- .../summary_section.test.tsx | 2 ++ .../summary_sections.test.tsx | 4 ++- .../summary_sections.tsx | 5 +-- .../document_creation_buttons.test.tsx | 3 ++ .../document_creation_buttons.tsx | 5 +-- .../document_creation_flyout.test.tsx | 5 +-- .../document_creation_flyout.tsx | 7 ++-- .../document_creation_logic.test.ts | 12 ++++--- .../document_creation_logic.ts | 2 +- .../document_creation_button.test.tsx | 3 ++ .../documents/document_creation_button.tsx | 3 +- .../documents/document_detail.test.tsx | 7 ++-- .../components/documents/document_detail.tsx | 9 ++--- .../documents/document_detail_logic.test.ts | 3 +- .../documents/document_detail_logic.ts | 3 +- .../components/documents/documents.test.tsx | 2 ++ .../components/documents/documents.tsx | 13 +++++--- .../build_search_ui_config.ts | 1 + .../search_experience/build_sort_options.ts | 2 +- .../customization_callout.test.tsx | 1 + .../customization_callout.tsx | 2 +- .../customization_modal.test.tsx | 2 ++ .../search_experience/customization_modal.tsx | 3 +- .../search_experience/hooks.test.tsx | 3 +- .../search_experience/pagination.test.tsx | 1 + .../search_experience/pagination.tsx | 1 + .../search_experience.test.tsx | 15 +++++---- .../search_experience/search_experience.tsx | 13 ++++---- .../search_experience_content.test.tsx | 8 +++-- .../search_experience_content.tsx | 16 +++++---- .../views/paging_view.test.tsx | 1 + .../views/result_view.test.tsx | 3 +- .../search_experience/views/result_view.tsx | 2 +- .../views/results_per_page_view.test.tsx | 1 + .../views/results_per_page_view.tsx | 2 +- .../views/search_box_view.test.tsx | 1 + .../views/sorting_view.test.tsx | 1 + .../search_experience/views/sorting_view.tsx | 2 +- .../components/engine/engine_logic.ts | 1 + .../components/engine/engine_nav.test.tsx | 2 ++ .../components/engine/engine_nav.tsx | 20 ++++++----- .../components/engine/engine_router.test.tsx | 7 ++-- .../components/engine/engine_router.tsx | 12 +++---- .../app_search/components/engine/types.ts | 2 +- .../components/recent_api_logs.test.tsx | 1 + .../components/recent_api_logs.tsx | 2 +- .../components/total_charts.test.tsx | 1 + .../components/total_charts.tsx | 7 ++-- .../components/total_stats.test.tsx | 2 ++ .../components/total_stats.tsx | 6 ++-- .../components/unavailable_prompt.test.tsx | 2 ++ .../components/unavailable_prompt.tsx | 2 +- .../engine_overview/engine_overview.test.tsx | 3 ++ .../engine_overview/engine_overview.tsx | 7 ++-- .../engine_overview_empty.test.tsx | 3 ++ .../engine_overview/engine_overview_empty.tsx | 2 +- .../engine_overview_metrics.test.tsx | 1 + .../engine_overview_metrics.tsx | 7 ++-- .../components/engines/assets/icons.test.tsx | 1 + .../engines/components/empty_state.test.tsx | 2 ++ .../engines/components/empty_state.tsx | 4 ++- .../engines/components/header.test.tsx | 1 + .../components/engines/components/header.tsx | 4 ++- .../engines/components/loading_state.test.tsx | 2 ++ .../engines/components/loading_state.tsx | 2 ++ .../components/engines/engines_logic.test.ts | 1 + .../engines/engines_overview.test.tsx | 1 + .../components/engines/engines_overview.tsx | 10 +++--- .../components/engines/engines_table.test.tsx | 3 ++ .../components/engines/engines_table.tsx | 13 ++++---- .../error_connecting.test.tsx | 2 ++ .../error_connecting/error_connecting.tsx | 1 + .../app_search/components/library/library.tsx | 3 +- .../components/log_retention_callout.test.tsx | 6 ++-- .../components/log_retention_callout.tsx | 5 +-- .../components/log_retention_tooltip.test.tsx | 3 ++ .../components/log_retention_tooltip.tsx | 3 +- .../log_retention/log_retention_logic.test.ts | 3 +- .../log_retention/log_retention_logic.ts | 2 +- .../log_retention/messaging/constants.tsx | 3 +- .../messaging/log_retention_message.test.tsx | 5 +-- .../messaging/log_retention_message.tsx | 1 + .../relevance_tuning/relevance_tuning.tsx | 3 +- .../relevance_tuning_logic.test.ts | 2 +- .../components/result/result.test.tsx | 6 ++-- .../app_search/components/result/result.tsx | 8 +++-- .../components/result/result_field.test.tsx | 1 + .../components/result/result_field.tsx | 4 ++- .../result/result_field_value.test.tsx | 1 + .../components/result/result_header.test.tsx | 1 + .../result/result_header_item.test.tsx | 1 + .../generic_confirmation_modal.test.tsx | 1 + .../generic_confirmation_modal.tsx | 2 +- .../log_retention_confirmation_modal.test.tsx | 2 ++ .../log_retention_confirmation_modal.tsx | 6 ++-- .../log_retention_panel.test.tsx | 2 ++ .../log_retention/log_retention_panel.tsx | 5 +-- .../components/settings/settings.test.tsx | 2 ++ .../components/settings/settings.tsx | 3 +- .../setup_guide/setup_guide.test.tsx | 2 ++ .../components/setup_guide/setup_guide.tsx | 6 ++-- .../applications/app_search/index.test.tsx | 9 +++-- .../public/applications/app_search/index.tsx | 29 ++++++++-------- .../error_connecting.test.tsx | 2 ++ .../error_connecting/error_connecting.tsx | 3 +- .../product_card/product_card.test.tsx | 4 ++- .../components/product_card/product_card.tsx | 6 ++-- .../product_selector.test.tsx | 4 ++- .../product_selector/product_selector.tsx | 7 ++-- .../setup_guide/setup_guide.test.tsx | 2 ++ .../components/setup_guide/setup_guide.tsx | 6 ++-- .../setup_guide/setup_guide_cta.test.tsx | 1 + .../setup_guide/setup_guide_cta.tsx | 4 ++- .../enterprise_search/index.test.tsx | 10 +++--- .../applications/enterprise_search/index.tsx | 7 ++-- .../public/applications/index.test.tsx | 12 ++++--- .../public/applications/index.tsx | 16 +++++---- .../error_state/error_state_prompt.test.tsx | 2 ++ .../shared/error_state/error_state_prompt.tsx | 4 ++- .../flash_messages/flash_messages.test.tsx | 2 ++ .../shared/flash_messages/flash_messages.tsx | 2 ++ .../flash_messages_logic.test.ts | 6 ++-- .../flash_messages/flash_messages_logic.ts | 7 +--- .../flash_messages/handle_api_errors.test.ts | 1 + .../flash_messages/handle_api_errors.ts | 3 +- .../shared/flash_messages/index.ts | 3 +- .../shared/flash_messages/types.ts | 14 ++++++++ .../shared/hidden_text/hidden_text.test.tsx | 1 + .../shared/hidden_text/hidden_text.tsx | 1 + .../indexing_status/indexing_status.test.tsx | 3 +- .../indexing_status/indexing_status.tsx | 4 +-- .../indexing_status_content.test.tsx | 1 + .../indexing_status_errors.test.tsx | 1 + .../indexing_status/indexing_status_logic.ts | 2 +- .../shared/kibana/kibana_logic.test.ts | 4 +-- .../shared/kibana/kibana_logic.ts | 9 ++--- .../kibana_chrome/generate_breadcrumbs.ts | 6 ++-- .../shared/kibana_chrome/set_chrome.test.tsx | 1 + .../shared/kibana_chrome/set_chrome.tsx | 1 + .../shared/layout/layout.test.tsx | 2 ++ .../applications/shared/layout/layout.tsx | 1 + .../shared/layout/side_nav.test.tsx | 4 ++- .../applications/shared/layout/side_nav.tsx | 5 +-- .../shared/loading/loading.test.tsx | 1 + .../applications/shared/loading/loading.tsx | 1 + .../shared/not_found/not_found.test.tsx | 2 ++ .../shared/not_found/not_found.tsx | 10 +++--- .../react_router_helpers/create_href.test.ts | 3 +- .../react_router_helpers/create_href.ts | 1 + .../eui_components.test.tsx | 6 ++-- .../react_router_helpers/eui_components.tsx | 5 ++- .../schema/schema_add_field_modal.test.tsx | 5 +-- .../schema/schema_errors_accordion.test.tsx | 2 ++ .../schema/schema_existing_field.test.tsx | 1 + .../setup_guide/cloud/instructions.test.tsx | 6 ++-- .../shared/setup_guide/cloud/instructions.tsx | 5 +-- .../shared/setup_guide/instructions.test.tsx | 6 ++-- .../shared/setup_guide/instructions.tsx | 5 +-- .../shared/setup_guide/setup_guide.test.tsx | 4 ++- .../shared/setup_guide/setup_guide.tsx | 3 +- .../shared/table_header/table_header.test.tsx | 2 ++ .../shared/telemetry/send_telemetry.test.tsx | 1 + .../shared/telemetry/send_telemetry.tsx | 1 + .../shared/telemetry/telemetry_logic.test.ts | 3 +- .../shared/truncate/truncate.test.tsx | 1 + .../__mocks__/content_sources.mock.ts | 5 +-- .../workplace_search/app_logic.test.ts | 2 +- .../layout/kibana_header_actions.test.tsx | 6 ++-- .../layout/kibana_header_actions.tsx | 2 +- .../components/layout/nav.test.tsx | 2 ++ .../components/layout/nav.tsx | 2 -- .../shared/api_key/api_key.test.tsx | 1 + .../component_loader.test.tsx | 1 + .../content_section/content_section.test.tsx | 5 ++- .../content_section/content_section.tsx | 1 - .../credential_item/credential_item.test.tsx | 1 + .../license_badge/license_badge.test.tsx | 1 + .../license_callout/license_callout.test.tsx | 1 + .../product_button/product_button.test.tsx | 2 ++ .../shared/product_button/product_button.tsx | 3 +- .../source_config_fields.test.tsx | 1 + .../source_config_fields.tsx | 1 - .../shared/source_icon/source_icon.test.tsx | 1 + .../shared/source_row/source_row.test.tsx | 4 ++- .../shared/source_row/source_row.tsx | 3 +- .../sources_table/sources_table.test.tsx | 6 ++-- .../shared/sources_table/sources_table.tsx | 2 +- .../table_pagination_bar.test.tsx | 1 + .../shared/user_icon/user_icon.test.tsx | 5 +-- .../shared/user_row/user_row.test.tsx | 5 +-- .../view_content_header.test.tsx | 2 ++ .../view_content_header.tsx | 1 - .../workplace_search/index.test.tsx | 4 ++- .../applications/workplace_search/index.tsx | 21 ++++++------ .../workplace_search/routes.test.tsx | 1 + .../components/add_source/add_source.test.tsx | 2 +- .../components/add_source/add_source.tsx | 8 ++--- .../add_source/add_source_header.test.tsx | 1 + .../add_source/add_source_list.test.tsx | 8 ++--- .../components/add_source/add_source_list.tsx | 11 +++---- .../add_source/add_source_logic.test.ts | 14 ++++---- .../components/add_source/add_source_logic.ts | 24 +++++--------- .../available_sources_list.test.tsx | 2 +- .../add_source/available_sources_list.tsx | 8 ++--- .../add_source/config_completed.test.tsx | 1 + .../add_source/config_completed.tsx | 8 ++--- .../add_source/config_docs_links.test.tsx | 1 + .../add_source/config_docs_links.tsx | 3 +- .../add_source/configuration_intro.test.tsx | 1 + .../add_source/configuration_intro.tsx | 9 +++-- .../add_source/configure_custom.test.tsx | 1 + .../add_source/configure_custom.tsx | 4 +-- .../add_source/configure_oauth.test.tsx | 1 + .../components/add_source/configure_oauth.tsx | 7 ++-- .../configured_sources_list.test.tsx | 5 +-- .../add_source/configured_sources_list.tsx | 2 +- .../add_source/connect_instance.test.tsx | 2 ++ .../add_source/connect_instance.tsx | 13 ++++---- .../add_source/re_authenticate.test.tsx | 1 + .../components/add_source/re_authenticate.tsx | 5 +-- .../add_source/save_config.test.tsx | 6 ++-- .../components/add_source/save_config.tsx | 13 +++----- .../add_source/save_custom.test.tsx | 1 + .../components/add_source/save_custom.tsx | 9 ++--- .../add_source/source_features.test.tsx | 4 +-- .../components/add_source/source_features.tsx | 5 ++- .../custom_source_icon.test.tsx | 1 + .../display_settings.test.tsx | 11 +++---- .../display_settings/display_settings.tsx | 17 ++++------ .../display_settings_logic.test.ts | 11 +++---- .../display_settings_logic.ts | 9 +++-- .../display_settings_router.test.tsx | 5 ++- .../display_settings_router.tsx | 3 +- .../example_result_detail_card.test.tsx | 4 +-- .../example_result_detail_card.tsx | 3 +- .../example_search_result_group.test.tsx | 5 ++- .../example_search_result_group.tsx | 6 ++-- .../example_standout_result.test.tsx | 5 ++- .../example_standout_result.tsx | 3 +- .../field_editor_modal.test.tsx | 6 ++-- .../display_settings/result_detail.test.tsx | 8 ++--- .../display_settings/result_detail.tsx | 3 +- .../display_settings/search_results.test.tsx | 7 ++-- .../display_settings/search_results.tsx | 5 ++- .../display_settings/subtitle_field.test.tsx | 1 + .../display_settings/title_field.test.tsx | 1 + .../components/overview.test.tsx | 4 +-- .../content_sources/components/overview.tsx | 33 ++++++++----------- .../components/schema/schema.test.tsx | 7 ++-- .../components/schema/schema.tsx | 15 ++++----- .../schema/schema_change_errors.test.tsx | 3 +- .../schema/schema_change_errors.tsx | 3 +- .../schema/schema_fields_table.test.tsx | 1 + .../components/schema/schema_fields_table.tsx | 6 ++-- .../components/schema/schema_logic.test.ts | 5 ++- .../components/schema/schema_logic.ts | 11 +++---- .../components/source_added.test.tsx | 4 +-- .../components/source_added.tsx | 2 +- .../components/source_content.test.tsx | 8 ++--- .../components/source_content.tsx | 19 ++++------- .../components/source_info_card.test.tsx | 1 + .../components/source_info_card.tsx | 1 - .../components/source_settings.test.tsx | 4 +-- .../components/source_settings.tsx | 20 +++++------ .../components/source_sub_nav.test.tsx | 5 +-- .../components/source_sub_nav.tsx | 8 ++--- .../organization_sources.test.tsx | 6 ++-- .../content_sources/organization_sources.tsx | 14 ++++---- .../views/content_sources/private_sources.tsx | 20 +++++------ .../views/content_sources/source_data.tsx | 4 +-- .../content_sources/source_logic.test.ts | 10 +++--- .../views/content_sources/source_logic.ts | 8 ++--- .../content_sources/source_router.test.tsx | 10 ++---- .../views/content_sources/source_router.tsx | 26 ++++++--------- .../content_sources/sources_logic.test.ts | 5 ++- .../views/content_sources/sources_logic.ts | 10 ++---- .../content_sources/sources_router.test.tsx | 4 +-- .../views/content_sources/sources_router.tsx | 16 ++++----- .../content_sources/sources_view.test.tsx | 4 +-- .../views/content_sources/sources_view.tsx | 7 ++-- .../views/error_state/error_state.test.tsx | 2 ++ .../views/error_state/error_state.tsx | 1 + .../groups/__mocks__/groups_logic.mock.ts | 3 +- .../components/add_group_modal.test.tsx | 5 +-- .../groups/components/add_group_modal.tsx | 3 +- .../components/clear_filters_link.test.tsx | 5 +-- .../groups/components/clear_filters_link.tsx | 2 +- .../components/filterable_users_list.test.tsx | 5 +-- .../components/filterable_users_list.tsx | 3 +- .../filterable_users_popover.test.tsx | 7 ++-- .../components/filterable_users_popover.tsx | 2 +- .../components/group_manager_modal.test.tsx | 7 ++-- .../groups/components/group_manager_modal.tsx | 10 ++---- .../groups/components/group_overview.test.tsx | 15 +++++---- .../groups/components/group_overview.tsx | 15 ++++----- .../groups/components/group_row.test.tsx | 5 +-- .../views/groups/components/group_row.tsx | 13 ++++---- .../group_row_sources_dropdown.test.tsx | 5 +-- .../components/group_row_sources_dropdown.tsx | 3 +- .../group_row_users_dropdown.test.tsx | 3 +- .../components/group_row_users_dropdown.tsx | 1 + .../group_source_prioritization.test.tsx | 5 +-- .../group_source_prioritization.tsx | 9 ++--- .../groups/components/group_sources.test.tsx | 8 ++--- .../views/groups/components/group_sources.tsx | 1 - .../groups/components/group_sub_nav.test.tsx | 5 +-- .../views/groups/components/group_sub_nav.tsx | 7 ++-- .../groups/components/group_users.test.tsx | 6 ++-- .../views/groups/components/group_users.tsx | 1 - .../components/group_users_table.test.tsx | 7 ++-- .../groups/components/group_users_table.tsx | 9 ++--- .../groups/components/groups_table.test.tsx | 12 +++---- .../views/groups/components/groups_table.tsx | 8 ++--- .../components/manage_users_modal.test.tsx | 3 +- .../components/shared_sources_modal.test.tsx | 3 +- .../components/source_option_item.test.tsx | 6 ++-- .../groups/components/source_option_item.tsx | 1 - .../groups/components/sources_list.test.tsx | 5 +-- .../table_filter_sources_dropdown.test.tsx | 4 +-- .../table_filter_sources_dropdown.tsx | 4 +-- .../table_filter_users_dropdown.test.tsx | 3 +- .../table_filter_users_dropdown.tsx | 4 +-- .../groups/components/table_filters.test.tsx | 5 +-- .../views/groups/components/table_filters.tsx | 3 +- .../components/user_option_item.test.tsx | 6 ++-- .../views/groups/group_logic.test.ts | 6 ++-- .../views/groups/group_logic.ts | 7 ++-- .../views/groups/group_router.test.tsx | 12 +++---- .../views/groups/group_router.tsx | 12 +++---- .../views/groups/groups.test.tsx | 15 ++++----- .../workplace_search/views/groups/groups.tsx | 14 +++----- .../views/groups/groups_logic.test.ts | 8 ++--- .../views/groups/groups_logic.ts | 12 +++---- .../views/groups/groups_router.test.tsx | 5 ++- .../views/groups/groups_router.tsx | 9 ++--- .../overview/__mocks__/overview_logic.mock.ts | 2 +- .../views/overview/onboarding_card.test.tsx | 1 + .../views/overview/onboarding_card.tsx | 3 +- .../views/overview/onboarding_steps.test.tsx | 6 ++-- .../views/overview/onboarding_steps.tsx | 18 +++++----- .../overview/organization_stats.test.tsx | 4 ++- .../views/overview/organization_stats.tsx | 8 ++--- .../views/overview/overview.test.tsx | 5 +-- .../views/overview/overview.tsx | 10 +++--- .../views/overview/overview_logic.ts | 1 + .../views/overview/recent_activity.test.tsx | 4 ++- .../views/overview/recent_activity.tsx | 10 +++--- .../views/overview/statistic_card.test.tsx | 1 + .../views/overview/statistic_card.tsx | 1 + .../components/private_sources_table.test.tsx | 2 ++ .../components/private_sources_table.tsx | 4 +-- .../views/security/security.test.tsx | 5 ++- .../views/security/security.tsx | 11 +++---- .../views/security/security_logic.test.ts | 6 ++-- .../views/security/security_logic.ts | 4 +-- .../settings/components/connectors.test.tsx | 2 +- .../views/settings/components/connectors.tsx | 7 ++-- .../settings/components/customize.test.tsx | 1 + .../views/settings/components/customize.tsx | 6 ++-- .../components/oauth_application.test.tsx | 7 ++-- .../settings/components/oauth_application.tsx | 13 ++++---- .../components/settings_sub_nav.test.tsx | 1 + .../settings/components/settings_sub_nav.tsx | 4 +-- .../components/source_config.test.tsx | 5 +-- .../settings/components/source_config.tsx | 8 ++--- .../views/settings/settings_logic.test.ts | 7 ++-- .../views/settings/settings_logic.ts | 7 ++-- .../views/settings/settings_router.test.tsx | 6 ++-- .../views/settings/settings_router.tsx | 9 ++--- .../views/setup_guide/setup_guide.test.tsx | 2 ++ .../views/setup_guide/setup_guide.tsx | 7 ++-- .../plugins/enterprise_search/public/index.ts | 1 + .../enterprise_search/public/plugin.ts | 6 ++-- .../server/__mocks__/router.mock.ts | 2 +- .../__mocks__/routerDependencies.mock.ts | 1 + .../server/collectors/app_search/telemetry.ts | 1 + .../collectors/enterprise_search/telemetry.ts | 1 + .../collectors/workplace_search/telemetry.ts | 1 + .../plugins/enterprise_search/server/index.ts | 3 +- .../server/lib/check_access.test.ts | 7 ++-- .../server/lib/check_access.ts | 5 +-- .../lib/enterprise_search_config_api.test.ts | 23 ++++++------- .../lib/enterprise_search_config_api.ts | 7 ++-- .../enterprise_search_request_handler.test.ts | 2 ++ .../lib/enterprise_search_request_handler.ts | 4 ++- .../enterprise_search/server/plugin.ts | 30 +++++++++-------- .../server/routes/app_search/engines.ts | 2 +- .../server/routes/app_search/index.ts | 6 ++-- .../routes/enterprise_search/config_data.ts | 2 +- .../enterprise_search/telemetry.test.ts | 3 +- .../routes/enterprise_search/telemetry.ts | 9 ++--- .../server/routes/workplace_search/index.ts | 6 ++-- .../saved_objects/app_search/telemetry.ts | 1 + .../enterprise_search/telemetry.ts | 1 + .../workplace_search/telemetry.ts | 1 + 489 files changed, 1376 insertions(+), 1032 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index 9430b9bf24466..7608bcb40a0b9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1119,6 +1119,39 @@ module.exports = { // All files files: ['x-pack/plugins/enterprise_search/**/*.{ts,tsx}'], rules: { + 'import/order': [ + 'error', + { + groups: ['unknown', ['builtin', 'external'], 'internal', 'parent', 'sibling', 'index'], + pathGroups: [ + { + pattern: + '{../../../../../../,../../../../../,../../../../,../../../,../../,../}{common/,*}__mocks__{*,/**}', + group: 'unknown', + }, + { + pattern: '{**,.}/*.mock', + group: 'unknown', + }, + { + pattern: 'react*', + group: 'external', + position: 'before', + }, + { + pattern: '{@elastic/**,@kbn/**,src/**}', + group: 'internal', + }, + ], + pathGroupsExcludedImportTypes: [], + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + 'newlines-between': 'always-and-inside-groups', + }, + ], + 'import/newline-after-import': 'error', 'react-hooks/exhaustive-deps': 'off', 'react/jsx-boolean-value': ['error', 'never'], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts index f284fef370f02..ecc7b991f0761 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -11,11 +11,11 @@ * NOTE: These variable names MUST start with 'mock*' in order for * Jest to accept its use within a jest.mock() */ +import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; +import { mockHttpValues } from './http_logic.mock'; import { mockKibanaValues } from './kibana_logic.mock'; import { mockLicensingValues } from './licensing_logic.mock'; -import { mockHttpValues } from './http_logic.mock'; import { mockTelemetryActions } from './telemetry_logic.mock'; -import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; export const mockAllValues = { ...mockKibanaValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index a201a2b56c25c..d8d66e5ee1998 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -6,6 +6,7 @@ */ import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; + import { mockHistory } from './'; export const mockKibanaValues = { diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx index 27e8a1421f462..2b5c06df37e8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; -import { act } from 'react-dom/test-utils'; + import { mount, ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { mountWithIntl } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx index a5a2891d3699c..3a98616082412 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { mount } from 'enzyme'; + import { I18nProvider } from '@kbn/i18n/react'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx index 224d71ac579a0..0127804374163 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow, mount, ReactWrapper } from 'enzyme'; + import { I18nProvider, __IntlProvider } from '@kbn/i18n/react'; // Use fake component to extract `intl` property to use in tests. diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts index 86f3993728e06..e5b0a702897bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { LogicMounter } from '../__mocks__'; -import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { AppLogic } from './app_logic'; describe('AppLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 8a55b7c0add94..c33a0e89d2aee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -8,6 +8,7 @@ import { kea, MakeLogicType } from 'kea'; import { InitialAppData } from '../../../common/types'; + import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx index 5248833d827b2..1a4e05c04f319 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx @@ -11,14 +11,15 @@ import { mockKibanaValues, setMockValues, setMockActions, rerender } from '../.. import React from 'react'; import { useParams } from 'react-router-dom'; + import { shallow } from 'enzyme'; -import { Loading } from '../../../shared/loading'; import { FlashMessages } from '../../../shared/flash_messages'; +import { Loading } from '../../../shared/loading'; import { LogRetentionCallout } from '../log_retention'; -import { AnalyticsHeader, AnalyticsUnavailable } from './components'; import { AnalyticsLayout } from './analytics_layout'; +import { AnalyticsHeader, AnalyticsUnavailable } from './components'; describe('AnalyticsLayout', () => { const { history } = mockKibanaValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx index 88d0f77541166..0c90267c1dbad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx @@ -7,18 +7,21 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; + import { useValues, useActions } from 'kea'; + import { EuiSpacer } from '@elastic/eui'; -import { KibanaLogic } from '../../../shared/kibana'; import { FlashMessages } from '../../../shared/flash_messages'; +import { KibanaLogic } from '../../../shared/kibana'; import { Loading } from '../../../shared/loading'; import { LogRetentionCallout, LogRetentionOptions } from '../log_retention'; -import { AnalyticsLogic } from './'; import { AnalyticsHeader, AnalyticsUnavailable } from './components'; +import { AnalyticsLogic } from './'; + interface Props { title: string; isQueryView?: boolean; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts index 6ca9eb23c962b..ad612e48c770a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts @@ -19,6 +19,7 @@ jest.mock('../engine', () => ({ import { nextTick } from '@kbn/test/jest'; import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants'; + import { AnalyticsLogic } from './'; describe('AnalyticsLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts index e978d2c65398e..de0828f6d71ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts @@ -8,9 +8,9 @@ import { kea, MakeLogicType } from 'kea'; import queryString from 'query-string'; -import { KibanaLogic } from '../../../shared/kibana'; -import { HttpLogic } from '../../../shared/http'; import { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import { EngineLogic } from '../engine'; import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx index 3f6bf77024c1e..3940151d3b7cd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx @@ -8,9 +8,11 @@ import '../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow } from 'enzyme'; + import { Route, Switch } from 'react-router-dom'; +import { shallow } from 'enzyme'; + import { AnalyticsRouter } from './'; describe('AnalyticsRouter', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx index 84ee392c2419e..8883d0d1ffcbd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiStat } from '@elastic/eui'; import { AnalyticsCards } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx index 417fa0cc48f65..b08e391f845e6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat } from '@elastic/eui'; interface Props { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx index dcea1f81e53eb..51238d62bdac7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx @@ -8,7 +8,9 @@ import { mockKibanaValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { Chart, Settings, LineSeries, Axis } from '@elastic/charts'; import { AnalyticsChart } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx index 686cadda02f63..fa33389503beb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; + import { useValues } from 'kea'; import moment from 'moment'; + import { Chart, Settings, LineSeries, CurveType, Axis } from '@elastic/charts'; import { KibanaLogic } from '../../../../shared/kibana'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx index 3faf2b03097f7..952c4c2517a0e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx @@ -8,13 +8,16 @@ import { setMockValues, mockKibanaValues } from '../../../../__mocks__'; import React, { ReactElement } from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; import moment, { Moment } from 'moment'; + import { EuiPageHeader, EuiSelect, EuiDatePickerRange, EuiButton } from '@elastic/eui'; import { LogRetentionTooltip } from '../../log_retention'; import { DEFAULT_START_DATE, DEFAULT_END_DATE } from '../constants'; + import { AnalyticsHeader } from './'; describe('AnalyticsHeader', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx index 3986f7859bfd2..8a87a5e8c211c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx @@ -6,12 +6,11 @@ */ import React, { useState } from 'react'; -import { useValues } from 'kea'; -import queryString from 'query-string'; +import { useValues } from 'kea'; import moment from 'moment'; +import queryString from 'query-string'; -import { i18n } from '@kbn/i18n'; import { EuiPageHeader, EuiPageHeaderSection, @@ -23,11 +22,12 @@ import { EuiDatePicker, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AnalyticsLogic } from '../'; import { KibanaLogic } from '../../../../shared/kibana'; import { LogRetentionTooltip, LogRetentionOptions } from '../../log_retention'; -import { AnalyticsLogic } from '../'; import { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants'; import { convertTagsToSelectOptions } from '../utils'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx index 4a3bbda5120bc..89fa5b4cc4b73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx @@ -9,7 +9,9 @@ import { mockKibanaValues } from '../../../../__mocks__'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFieldSearch } from '@elastic/eui'; import { AnalyticsSearch } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx index 922e096701e84..4f2b525aaa168 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx @@ -6,10 +6,11 @@ */ import React, { useState } from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { KibanaLogic } from '../../../../shared/kibana'; import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH } from '../../../routes'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx index 981173e2a915b..56e30e6061173 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsSection } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx index 0788edfdda427..2eac65fc21091 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx @@ -9,6 +9,7 @@ import { mountWithIntl, mockKibanaValues } from '../../../../../__mocks__'; import '../../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { EuiBasicTable, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; import { AnalyticsTable } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx index 8e9853233cbed..a580047f1f635 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx @@ -6,10 +6,12 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; + import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Query } from '../../types'; + import { TERM_COLUMN_PROPS, TAGS_COLUMN, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx index 9ad2cc32f99c5..9204fa6e75fa7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { InlineTagsList } from './inline_tags_list'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx index 421ff1eedf278..908b096c80a9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; + import { EuiBadgeGroup, EuiBadge, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Query } from '../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx index 4396f91136258..cc8f13299c57f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx @@ -9,6 +9,7 @@ import { mountWithIntl } from '../../../../../__mocks__'; import '../../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { EuiBasicTable, EuiLink, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; import { QueryClicksTable } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx index 7c333623df6c0..4a93724ff5245 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx @@ -7,15 +7,16 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../../../routes'; -import { generateEnginePath } from '../../../engine'; import { DOCUMENTS_TITLE } from '../../../documents'; +import { generateEnginePath } from '../../../engine'; import { QueryClick } from '../../types'; + import { FIRST_COLUMN_PROPS, TAGS_COLUMN, COUNT_COLUMN_PROPS } from './shared_columns'; interface Props { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx index fdbbd326c47a1..a5a582d3747bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx @@ -9,6 +9,7 @@ import { mountWithIntl, mockKibanaValues } from '../../../../../__mocks__'; import '../../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { EuiBasicTable, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; import { RecentQueriesTable } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx index 20e50e633b321..7724ac5c393ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx @@ -7,11 +7,12 @@ import React from 'react'; +import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedTime } from '@kbn/i18n/react'; -import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; import { RecentQuery } from '../../types'; + import { TERM_COLUMN_PROPS, TAGS_COLUMN, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx index 0612fac1c07ed..9d8365a2f7af1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx @@ -6,14 +6,15 @@ */ import React from 'react'; + import { i18n } from '@kbn/i18n'; -import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { KibanaLogic } from '../../../../../shared/kibana'; +import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH } from '../../../../routes'; import { generateEnginePath } from '../../../engine'; - import { Query, RecentQuery } from '../../types'; + import { InlineTagsList } from './inline_tags_list'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.test.tsx index ddc0e4636b3ad..e2ff440615dfc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiEmptyPrompt } from '@elastic/eui'; import { FlashMessages } from '../../../../shared/flash_messages'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.tsx index 2ef020d2f4992..388570b32b6d2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; + import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FlashMessages } from '../../../../shared/flash_messages'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts index a04a9474ce5ae..75001f5bc86d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts @@ -6,6 +6,7 @@ */ import moment from 'moment'; + import { i18n } from '@kbn/i18n'; export const ANALYTICS_TITLE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts index 2d00c906b2aec..db679b0f387e8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts @@ -6,11 +6,12 @@ */ import moment from 'moment'; -import { i18n } from '@kbn/i18n'; + import { EuiSelectProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { SERVER_DATE_FORMAT } from './constants'; import { ChartData } from './components/analytics_chart'; +import { SERVER_DATE_FORMAT } from './constants'; interface ConvertToChartData { data: number[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx index 065b2208648bf..d8921ff0d3723 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx @@ -9,6 +9,7 @@ import { setMockValues } from '../../../../__mocks__'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { @@ -18,6 +19,7 @@ import { AnalyticsTable, RecentQueriesTable, } from '../components'; + import { Analytics, ViewAllButton } from './analytics'; describe('Analytics overview', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx index 09b1ff45c6122..a4f0bc356ac78 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { @@ -21,6 +22,8 @@ import { } from '../../../routes'; import { generateEnginePath } from '../../engine'; +import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsSection, AnalyticsTable, RecentQueriesTable } from '../components'; import { ANALYTICS_TITLE, TOTAL_QUERIES, @@ -32,9 +35,7 @@ import { TOP_QUERIES_NO_CLICKS, RECENT_QUERIES, } from '../constants'; -import { AnalyticsLayout } from '../analytics_layout'; -import { AnalyticsSection, AnalyticsTable, RecentQueriesTable } from '../components'; -import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../'; +import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../index'; export const Analytics: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx index 050770944edcd..978f11ddfd5cd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx @@ -10,12 +10,14 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; import { useParams } from 'react-router-dom'; + import { shallow } from 'enzyme'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsCards, AnalyticsChart, QueryClicksTable } from '../components'; + import { QueryDetail } from './'; describe('QueryDetail', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx index 96587eb528710..f00c4e29a7190 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; @@ -17,7 +18,7 @@ import { useDecodedParams } from '../../../utils/encode_path_params'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSection, QueryClicksTable } from '../components'; -import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../'; +import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../index'; const QUERY_DETAIL_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.title', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx index 40577fb2d4447..21d515a7b9795 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { RecentQueriesTable } from '../components'; + import { RecentQueries } from './'; describe('RecentQueries', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx index e5380258894ae..bb0c3c4d32244 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { RECENT_QUERIES } from '../constants'; +import { AnalyticsLogic } from '../'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSearch, RecentQueriesTable } from '../components'; -import { AnalyticsLogic } from '../'; +import { RECENT_QUERIES } from '../constants'; export const RecentQueries: React.FC = () => { const { recentQueries } = useValues(AnalyticsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx index b037e6bf1d64e..46b2b37958435 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsTable } from '../components'; + import { TopQueries } from './'; describe('TopQueries', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx index 76d523d16ee11..6459126560b3a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { TOP_QUERIES } from '../constants'; +import { AnalyticsLogic } from '../'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSearch, AnalyticsTable } from '../components'; -import { AnalyticsLogic } from '../'; +import { TOP_QUERIES } from '../constants'; export const TopQueries: React.FC = () => { const { topQueries } = useValues(AnalyticsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx index 1248a49fc5a9c..83212160d1350 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsTable } from '../components'; + import { TopQueriesNoClicks } from './'; describe('TopQueriesNoClicks', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx index 604ab96b871e7..8e2591697feaa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { TOP_QUERIES_NO_CLICKS } from '../constants'; +import { AnalyticsLogic } from '../'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSearch, AnalyticsTable } from '../components'; -import { AnalyticsLogic } from '../'; +import { TOP_QUERIES_NO_CLICKS } from '../constants'; export const TopQueriesNoClicks: React.FC = () => { const { topQueriesNoClicks } = useValues(AnalyticsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx index 3cb77b3c7afbc..dfc5b9c93ab64 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsTable } from '../components'; + import { TopQueriesNoResults } from './'; describe('TopQueriesNoResults', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx index 425fdf8e88559..e093a5130d204 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { TOP_QUERIES_NO_RESULTS } from '../constants'; +import { AnalyticsLogic } from '../'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSearch, AnalyticsTable } from '../components'; -import { AnalyticsLogic } from '../'; +import { TOP_QUERIES_NO_RESULTS } from '../constants'; export const TopQueriesNoResults: React.FC = () => { const { topQueriesNoResults } = useValues(AnalyticsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx index 83be03e95d2cf..fb967ca06b387 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsTable } from '../components'; + import { TopQueriesWithClicks } from './'; describe('TopQueriesWithClicks', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx index bec096019035b..87e276a8382c3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { TOP_QUERIES_WITH_CLICKS } from '../constants'; +import { AnalyticsLogic } from '../'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSearch, AnalyticsTable } from '../components'; -import { AnalyticsLogic } from '../'; +import { TOP_QUERIES_WITH_CLICKS } from '../constants'; export const TopQueriesWithClicks: React.FC = () => { const { topQueriesWithClicks } = useValues(AnalyticsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index 2e28e5a272643..0fb118548a67b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; + import { DOCS_PREFIX } from '../../routes'; export const CREDENTIALS_TITLE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx index cc783e7c056e2..48fcf4b8c5b66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx @@ -9,12 +9,15 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { Credentials } from './credentials'; import { EuiCopy, EuiLoadingContent, EuiPageContentBody } from '@elastic/eui'; import { externalUrl } from '../../../shared/enterprise_search_url'; + +import { Credentials } from './credentials'; + import { CredentialsFlyout } from './credentials_flyout'; describe('Credentials', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx index 0266b64f97104..266e9467c300d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -6,6 +6,7 @@ */ import React, { useEffect } from 'react'; + import { useActions, useValues } from 'kea'; import { @@ -24,14 +25,14 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { CredentialsLogic } from './credentials_logic'; -import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; import { CREDENTIALS_TITLE } from './constants'; -import { CredentialsList } from './credentials_list'; import { CredentialsFlyout } from './credentials_flyout'; +import { CredentialsList } from './credentials_list'; +import { CredentialsLogic } from './credentials_logic'; export const Credentials: React.FC = () => { const { initializeCredentialsData, resetCredentials, showCredentialsForm } = useActions( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx index 8b5a59b82c19b..595bc1bcbb828 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx @@ -8,12 +8,15 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyoutBody, EuiForm } from '@elastic/eui'; import { ApiTokenTypes } from '../constants'; import { defaultApiToken } from '../credentials_logic'; +import { CredentialsFlyoutBody } from './body'; import { FormKeyName, FormKeyType, @@ -21,7 +24,6 @@ import { FormKeyEngineAccess, FormKeyUpdateWarning, } from './form_components'; -import { CredentialsFlyoutBody } from './body'; describe('CredentialsFlyoutBody', () => { const values = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx index f3de25fe0a25d..def165f3f82a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiFlyoutBody, EuiForm } from '@elastic/eui'; import { FlashMessages } from '../../../../shared/flash_messages'; -import { CredentialsLogic } from '../credentials_logic'; import { ApiTokenTypes } from '../constants'; +import { CredentialsLogic } from '../credentials_logic'; import { FormKeyName, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx index 036fe881c7d0d..23e85b92bb8b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyoutFooter, EuiButtonEmpty } from '@elastic/eui'; import { CredentialsFlyoutFooter } from './footer'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx index dc2d52a073b36..c05bd82c6206e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiFlyoutFooter, EuiFlexGroup, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx index 51a737ce8c826..7247deb09f12b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions, rerender } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiRadio, EuiCheckbox } from '@elastic/eui'; import { FormKeyEngineAccess, EngineSelection } from './key_engine_access'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx index 2a9e8cf153dca..0d6ebfe437927 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiFormRow, EuiRadio, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx index 27f95f2ba7cd8..d54d0c89c90cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFieldText, EuiFormRow } from '@elastic/eui'; import { FormKeyName } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx index cb4dce76dfcc1..f4f4f5f0aaaaa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiFormRow, EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx index 8cfa5b3c4571a..cf45576d691cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCheckbox } from '@elastic/eui'; import { FormKeyReadWriteAccess } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx index f9653159b4403..0b631089c3984 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiCheckbox, EuiText, EuiTitle, EuiSpacer, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx index 9cf6c82184579..5de2c7fda53ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx @@ -8,10 +8,13 @@ import { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiSelect } from '@elastic/eui'; import { ApiTokenTypes, TOKEN_TYPE_INFO } from '../../constants'; + import { FormKeyType } from './'; describe('FormKeyType', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx index a8cc16b3b30e7..60308274fbb76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx @@ -6,13 +6,15 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiFormRow, EuiSelect, EuiText, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AppLogic } from '../../../../app_logic'; -import { CredentialsLogic } from '../../credentials_logic'; import { TOKEN_TYPE_DESCRIPTION, TOKEN_TYPE_INFO, DOCS_HREF } from '../../constants'; +import { CredentialsLogic } from '../../credentials_logic'; export const FormKeyType: React.FC = () => { const { myRole } = useValues(AppLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx index 073c4ec1c92bf..38eec0b371576 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCallOut } from '@elastic/eui'; import { FormKeyUpdateWarning } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx index 87cda9590f5cb..c24eebea9178b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiSpacer, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx index 0772a395dbe71..8ee7f810c1fa5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx @@ -8,7 +8,9 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyoutHeader } from '@elastic/eui'; import { ApiTokenTypes } from '../constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx index a9efcbe371c4f..586ddc5c22b97 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; + import { useValues } from 'kea'; + import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { CredentialsLogic } from '../credentials_logic'; import { FLYOUT_ARIA_LABEL_ID } from '../constants'; +import { CredentialsLogic } from '../credentials_logic'; export const CredentialsFlyoutHeader: React.FC = () => { const { activeApiToken } = useValues(CredentialsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx index 1f7408857857a..9932b8ca227b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx @@ -8,7 +8,9 @@ import { setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyout } from '@elastic/eui'; import { CredentialsFlyout } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx index 1335a3cdeea18..2ee73a6b80b5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx @@ -6,14 +6,17 @@ */ import React from 'react'; + import { useActions } from 'kea'; + import { EuiPortal, EuiFlyout } from '@elastic/eui'; -import { CredentialsLogic } from '../credentials_logic'; import { FLYOUT_ARIA_LABEL_ID } from '../constants'; -import { CredentialsFlyoutHeader } from './header'; +import { CredentialsLogic } from '../credentials_logic'; + import { CredentialsFlyoutBody } from './body'; import { CredentialsFlyoutFooter } from './footer'; +import { CredentialsFlyoutHeader } from './header'; export const CredentialsFlyout: React.FC = () => { const { hideCredentialsForm } = useActions(CredentialsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx index dd3d8ef8069ba..8c52df30bfc67 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx @@ -8,15 +8,18 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiBasicTable, EuiCopy, EuiEmptyPrompt } from '@elastic/eui'; -import { ApiToken } from '../types'; +import { HiddenText } from '../../../../shared/hidden_text'; import { ApiTokenTypes } from '../constants'; +import { ApiToken } from '../types'; -import { HiddenText } from '../../../../shared/hidden_text'; import { Key } from './key'; -import { CredentialsList } from './credentials_list'; + +import { CredentialsList } from './'; describe('Credentials', () => { const apiToken: ApiToken = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx index 9d220469347f2..f23479017a680 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx @@ -6,19 +6,21 @@ */ import React, { useMemo } from 'react'; -import { EuiBasicTable, EuiBasicTableColumn, EuiCopy, EuiEmptyPrompt } from '@elastic/eui'; -import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; + import { useActions, useValues } from 'kea'; +import { EuiBasicTable, EuiBasicTableColumn, EuiCopy, EuiEmptyPrompt } from '@elastic/eui'; +import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; import { i18n } from '@kbn/i18n'; -import { CredentialsLogic } from '../credentials_logic'; -import { Key } from './key'; import { HiddenText } from '../../../../shared/hidden_text'; -import { ApiToken } from '../types'; import { TOKEN_TYPE_DISPLAY_NAMES } from '../constants'; -import { apiTokenSort } from '../utils/api_token_sort'; +import { CredentialsLogic } from '../credentials_logic'; +import { ApiToken } from '../types'; import { getModeDisplayText, getEnginesDisplayText } from '../utils'; +import { apiTokenSort } from '../utils/api_token_sort'; + +import { Key } from './key'; export const CredentialsList: React.FC = () => { const { deleteApiKey, fetchCredentials, showCredentialsForm } = useActions(CredentialsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx index c18302db9ddfd..5e042319ae613 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiButtonIcon } from '@elastic/eui'; import { Key } from './key'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx index 940453c83a1fe..ff14379b9aecc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index 005f487772d80..c9d6a43ebbbae 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -17,6 +17,7 @@ jest.mock('../../app_logic', () => ({ import { nextTick } from '@kbn/test/jest'; import { AppLogic } from '../../app_logic'; + import { ApiTokenTypes } from './constants'; import { CredentialsLogic } from './credentials_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts index 25cd1be93836d..ff4600872c589 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts @@ -7,19 +7,19 @@ import { kea, MakeLogicType } from 'kea'; -import { formatApiName } from '../../utils/format_api_name'; -import { ApiTokenTypes, CREATE_MESSAGE, UPDATE_MESSAGE, DELETE_MESSAGE } from './constants'; - -import { HttpLogic } from '../../../shared/http'; +import { Meta } from '../../../../../common/types'; import { clearFlashMessages, setSuccessMessage, flashAPIErrors, } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; import { AppLogic } from '../../app_logic'; - -import { Meta } from '../../../../../common/types'; import { Engine } from '../../types'; +import { formatApiName } from '../../utils/format_api_name'; + +import { ApiTokenTypes, CREATE_MESSAGE, UPDATE_MESSAGE, DELETE_MESSAGE } from './constants'; + import { ApiToken, CredentialsDetails, TokenReadWrite } from './types'; export const defaultApiToken: ApiToken = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts index ddc81658eed2c..0427d25add49b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts @@ -6,6 +6,7 @@ */ import { Engine } from '../../types'; + import { ApiTokenTypes } from './constants'; export interface CredentialsDetails { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts index 1f84caa7e1ef7..70277d6cb7f22 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { apiTokenSort } from '.'; import { ApiTokenTypes } from '../constants'; import { ApiToken } from '../types'; +import { apiTokenSort } from '.'; + describe('apiTokenSort', () => { const apiToken: ApiToken = { name: '', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx index e92957405a524..71d00efa2a868 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx @@ -6,22 +6,24 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; -import { getEnginesDisplayText } from './get_engines_display_text'; -import { ApiToken } from '../types'; import { ApiTokenTypes } from '../constants'; +import { ApiToken } from '../types'; -const apiToken: ApiToken = { - name: '', - type: ApiTokenTypes.Private, - read: true, - write: true, - access_all_engines: true, - engines: ['engine1', 'engine2', 'engine3'], -}; +import { getEnginesDisplayText } from './get_engines_display_text'; describe('getEnginesDisplayText', () => { + const apiToken: ApiToken = { + name: '', + type: ApiTokenTypes.Private, + read: true, + write: true, + access_all_engines: true, + engines: ['engine1', 'engine2', 'engine3'], + }; + it('returns "--" when the token is an admin token', () => { const wrapper = shallow(
{getEnginesDisplayText({ ...apiToken, type: ApiTokenTypes.Admin })}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx index 34089cacbf180..d3577ec14fec9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { ApiTokenTypes, ALL } from '../constants'; import { ApiToken } from '../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx index 7203cf6982086..34afa9d1e39ed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx @@ -9,7 +9,9 @@ import '../../../../__mocks__/enterprise_search_url.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiCode, EuiCodeBlock, EuiButtonEmpty } from '@elastic/eui'; import { ApiCodeExample, FlyoutHeader, FlyoutBody, FlyoutFooter } from './api_code_example'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx index 9167df25f75b5..88e9df5c2bbf5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import dedent from 'dedent'; import React from 'react'; + +import dedent from 'dedent'; + import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyoutHeader, EuiTitle, @@ -27,18 +27,19 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; +import { DOCS_PREFIX } from '../../../routes'; import { EngineLogic } from '../../engine'; import { EngineDetails } from '../../engine/types'; - -import { DOCS_PREFIX } from '../../../routes'; import { DOCUMENTS_API_JSON_EXAMPLE, FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON, } from '../constants'; -import { DocumentCreationLogic } from '../'; +import { DocumentCreationLogic } from '../index'; export const ApiCodeExample: React.FC = () => ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx index c1c0a554b4794..8b5b36094fbc6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx @@ -8,10 +8,13 @@ import { setMockValues, setMockActions, rerender } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiTextArea, EuiButtonEmpty, EuiButton } from '@elastic/eui'; import { Errors } from '../creation_response_components'; + import { PasteJsonText, FlyoutHeader, FlyoutBody, FlyoutFooter } from './paste_json_text'; describe('PasteJsonText', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx index 377d795413714..2d4a6de26333f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlyoutHeader, EuiTitle, @@ -22,12 +22,13 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { AppLogic } from '../../../app_logic'; import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON, FLYOUT_CONTINUE_BUTTON } from '../constants'; import { Errors } from '../creation_response_components'; -import { DocumentCreationLogic } from '../'; +import { DocumentCreationLogic } from '../index'; import './paste_json_text.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx index 2c66ae56dd3ce..739580d039a36 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx @@ -8,10 +8,13 @@ import { setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiButtonEmpty } from '@elastic/eui'; import { DocumentCreationButtons } from '../'; + import { ShowCreationModes } from './'; describe('ShowCreationModes', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx index b67c7689d816f..d46b9acbb63d7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; + import { useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlyoutHeader, EuiTitle, @@ -16,9 +16,10 @@ import { EuiFlyoutFooter, EuiButtonEmpty, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON } from '../constants'; -import { DocumentCreationLogic, DocumentCreationButtons } from '../'; +import { DocumentCreationLogic, DocumentCreationButtons } from '../index'; export const ShowCreationModes: React.FC = () => { const { closeDocumentCreation } = useActions(DocumentCreationLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx index cee76ebe6857e..7dc8952a18688 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx @@ -8,10 +8,13 @@ import { setMockValues, setMockActions, rerender } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFilePicker, EuiButtonEmpty, EuiButton } from '@elastic/eui'; import { Errors } from '../creation_response_components'; + import { UploadJsonFile, FlyoutHeader, FlyoutBody, FlyoutFooter } from './upload_json_file'; describe('UploadJsonFile', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx index cab79a929f7b9..5d50ae55fcd10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlyoutHeader, EuiTitle, @@ -22,12 +22,13 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { AppLogic } from '../../../app_logic'; import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON, FLYOUT_CONTINUE_BUTTON } from '../constants'; import { Errors } from '../creation_response_components'; -import { DocumentCreationLogic } from '../'; +import { DocumentCreationLogic } from '../index'; export const UploadJsonFile: React.FC = () => ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.test.tsx index 7ac97ae81b6ca..f03989aeaf5a3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.test.tsx @@ -8,7 +8,9 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCallOut } from '@elastic/eui'; import { Errors } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.tsx index 618828182e67d..3564d8ad088ee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; import { EuiCallOut } from '@elastic/eui'; import { DOCUMENT_CREATION_ERRORS, DOCUMENT_CREATION_WARNINGS } from '../constants'; -import { DocumentCreationLogic } from '../'; +import { DocumentCreationLogic } from '../index'; export const Errors: React.FC = () => { const { errors, warnings } = useValues(DocumentCreationLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx index 9558d23fa3a77..f53f94322879c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx @@ -8,15 +8,19 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyoutBody, EuiCallOut, EuiButton } from '@elastic/eui'; +import { FlyoutHeader, FlyoutBody, FlyoutFooter } from './summary'; import { InvalidDocumentsSummary, ValidDocumentsSummary, SchemaFieldsSummary, } from './summary_sections'; -import { Summary, FlyoutHeader, FlyoutBody, FlyoutFooter } from './summary'; + +import { Summary } from './'; describe('Summary', () => { const values = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.tsx index 673c6726afb5d..8361afe62e1ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlyoutHeader, EuiTitle, @@ -19,10 +19,11 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DocumentCreationLogic } from '../'; import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CLOSE_BUTTON, DOCUMENT_CREATION_ERRORS } from '../constants'; import { DocumentCreationStep } from '../types'; -import { DocumentCreationLogic } from '../'; import { InvalidDocumentsSummary, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.test.tsx index 0704d465bbac4..cd8209bafed3f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; import { ExampleDocumentJson, MoreDocumentsText } from './summary_documents'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx index be19a7677a1ab..0dad75cb1f98f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx @@ -7,8 +7,8 @@ import React, { Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiCodeBlock, EuiCallOut, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; interface ExampleDocumentJsonProps { document: object; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx index 41028d61c55f2..24fa2766cb15d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx @@ -6,7 +6,9 @@ */ import React, { ReactElement } from 'react'; + import { shallow } from 'enzyme'; + import { EuiAccordion, EuiIcon } from '@elastic/eui'; import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx index 9ead42f33521f..7eb9f3f46036d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx @@ -8,11 +8,13 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiBadge } from '@elastic/eui'; -import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; + import { ExampleDocumentJson, MoreDocumentsText } from './summary_documents'; +import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; import { InvalidDocumentsSummary, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx index 637188132d6bc..f2e863c2a9983 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx @@ -6,15 +6,16 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DocumentCreationLogic } from '../'; -import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; import { ExampleDocumentJson, MoreDocumentsText } from './summary_documents'; +import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; export const InvalidDocumentsSummary: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx index 4b90acfbc37a8..7cbcc6b17e047 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx @@ -9,8 +9,11 @@ import { setMockActions } from '../../../__mocks__/kea.mock'; import '../../__mocks__/engine_logic.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCard } from '@elastic/eui'; + import { EuiCardTo } from '../../../shared/react_router_helpers'; import { DocumentCreationButtons } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx index ec9c6615f5b8c..6d3caca87dcc3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx @@ -6,10 +6,9 @@ */ import React from 'react'; + import { useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiCode, @@ -20,6 +19,8 @@ import { EuiCard, EuiIcon, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCardTo } from '../../../shared/react_router_helpers'; import { DOCS_PREFIX, ENGINE_CRAWLER_PATH } from '../../routes'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx index 4c5375d78f95f..66995b8d20dfe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyout } from '@elastic/eui'; import { @@ -18,9 +20,8 @@ import { UploadJsonFile, } from './creation_mode_components'; import { Summary } from './creation_response_components'; -import { DocumentCreationStep } from './types'; - import { DocumentCreationFlyout, FlyoutContent } from './document_creation_flyout'; +import { DocumentCreationStep } from './types'; describe('DocumentCreationFlyout', () => { const values = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx index 16f805d7e86fd..159f3403d3740 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx @@ -6,14 +6,12 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; import { EuiPortal, EuiFlyout } from '@elastic/eui'; -import { DocumentCreationLogic } from './'; -import { DocumentCreationStep } from './types'; import { FLYOUT_ARIA_LABEL_ID } from './constants'; - import { ShowCreationModes, ApiCodeExample, @@ -21,6 +19,9 @@ import { UploadJsonFile, } from './creation_mode_components'; import { Summary } from './creation_response_components'; +import { DocumentCreationStep } from './types'; + +import { DocumentCreationLogic } from './'; export const DocumentCreationFlyout: React.FC = () => { const { closeDocumentCreation } = useActions(DocumentCreationLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts index 63c59343580d3..37d3d1577767f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts @@ -7,13 +7,9 @@ import { LogicMounter, mockHttpValues } from '../../../__mocks__'; -import { nextTick } from '@kbn/test/jest'; import dedent from 'dedent'; -jest.mock('./utils', () => ({ - readUploadedFileAsText: jest.fn(), -})); -import { readUploadedFileAsText } from './utils'; +import { nextTick } from '@kbn/test/jest'; jest.mock('../engine', () => ({ EngineLogic: { values: { engineName: 'test-engine' } }, @@ -21,6 +17,12 @@ jest.mock('../engine', () => ({ import { DOCUMENTS_API_JSON_EXAMPLE } from './constants'; import { DocumentCreationStep } from './types'; + +jest.mock('./utils', () => ({ + readUploadedFileAsText: jest.fn(), +})); +import { readUploadedFileAsText } from './utils'; + import { DocumentCreationLogic } from './'; describe('DocumentCreationLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts index 13d2618bcd31f..a0ef73bbcea21 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { kea, MakeLogicType } from 'kea'; import dedent from 'dedent'; +import { kea, MakeLogicType } from 'kea'; import { isPlainObject, chunk, uniq } from 'lodash'; import { HttpLogic } from '../../../shared/http'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx index ab1679c455c6e..82fa9d3c82ce9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx @@ -8,10 +8,13 @@ import { setMockActions } from '../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiButton } from '@elastic/eui'; import { DocumentCreationFlyout } from '../document_creation'; + import { DocumentCreationButton } from './document_creation_button'; describe('DocumentCreationButton', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx index a05005fefa082..687f589d37594 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; + import { useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DocumentCreationLogic, DocumentCreationFlyout } from '../document_creation'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx index 55613077efdba..ba060b7497270 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx @@ -10,14 +10,17 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import React from 'react'; -import { shallow } from 'enzyme'; import { useParams } from 'react-router-dom'; + +import { shallow } from 'enzyme'; + import { EuiPageContent, EuiBasicTable } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; -import { DocumentDetail } from '.'; import { ResultFieldValue } from '../result'; +import { DocumentDetail } from '.'; + describe('DocumentDetail', () => { const values = { dataLoading: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx index ca6af345de7ed..8f80978c29002 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx @@ -6,9 +6,10 @@ */ import React, { useEffect } from 'react'; -import { useActions, useValues } from 'kea'; import { useParams } from 'react-router-dom'; +import { useActions, useValues } from 'kea'; + import { EuiButton, EuiPageHeader, @@ -21,15 +22,15 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Loading } from '../../../shared/loading'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; import { useDecodedParams } from '../../utils/encode_path_params'; import { ResultFieldValue } from '../result'; +import { DOCUMENTS_TITLE } from './constants'; import { DocumentDetailLogic } from './document_detail_logic'; import { FieldDetails } from './types'; -import { DOCUMENTS_TITLE } from './constants'; const DOCUMENT_DETAIL_TITLE = (documentId: string) => i18n.translate('xpack.enterpriseSearch.appSearch.documentDetail.title', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index ef5ebad3aea13..d2683fac649a0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -15,9 +15,10 @@ import { mockEngineValues } from '../../__mocks__'; import { nextTick } from '@kbn/test/jest'; -import { DocumentDetailLogic } from './document_detail_logic'; import { InternalSchemaTypes } from '../../../shared/types'; +import { DocumentDetailLogic } from './document_detail_logic'; + describe('DocumentDetailLogic', () => { const { mount } = new LogicMounter(DocumentDetailLogic); const { http } = mockHttpValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts index 8b023fb585f86..17c2c788523d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts @@ -6,11 +6,12 @@ */ import { kea, MakeLogicType } from 'kea'; + import { i18n } from '@kbn/i18n'; import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; -import { KibanaLogic } from '../../../shared/kibana'; import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_DOCUMENTS_PATH } from '../../routes'; import { EngineLogic, generateEnginePath } from '../engine'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx index 43bbc6cc67895..ace76ae55c046 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx @@ -8,10 +8,12 @@ import { setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { DocumentCreationButton } from './document_creation_button'; import { SearchExperience } from './search_experience'; + import { Documents } from '.'; describe('Documents', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx index 7223900911512..8c3ae7fd24f6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx @@ -7,16 +7,19 @@ import React from 'react'; -import { EuiPageHeader, EuiPageHeaderSection, EuiTitle, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { useValues } from 'kea'; + +import { EuiPageHeader, EuiPageHeaderSection, EuiTitle, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DocumentCreationButton } from './document_creation_button'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; -import { DOCUMENTS_TITLE } from './constants'; -import { EngineLogic } from '../engine'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; + import { AppLogic } from '../../app_logic'; +import { EngineLogic } from '../engine'; + +import { DOCUMENTS_TITLE } from './constants'; +import { DocumentCreationButton } from './document_creation_button'; import { SearchExperience } from './search_experience'; interface Props { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts index b9577d9d0f07d..9fac068555db5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts @@ -6,6 +6,7 @@ */ import { Schema } from '../../../../shared/types'; + import { Fields } from './types'; export const buildSearchUIConfig = (apiConnector: object, schema: Schema, fields: Fields) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts index ab3a943ef2f55..54cf2bdd4f257 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts @@ -7,8 +7,8 @@ import { flatten } from 'lodash'; -import { Fields, SortOption, SortDirection } from './types'; import { ASCENDING, DESCENDING } from './constants'; +import { Fields, SortOption, SortDirection } from './types'; const fieldNameToSortOptions = (fieldName: string): SortOption[] => ['asc', 'desc'].map((direction) => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx index e62e4521927dc..6ed2d7edc9639 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiButton } from '@elastic/eui'; import { CustomizationCallout } from './customization_callout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx index 8954549f74651..48a9fcdeaa878 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFlexGroup, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; interface Props { onClick(): void; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx index 11e13f4222abb..332c5b822eb6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { CustomizationModal } from './customization_modal'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx index 2d3604b2ba279..e05fc10053ff1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx @@ -7,6 +7,8 @@ import React, { useState, useMemo } from 'react'; +import { useValues } from 'kea'; + import { EuiButton, EuiButtonEmpty, @@ -21,7 +23,6 @@ import { EuiOverlayMask, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useValues } from 'kea'; import { EngineLogic } from '../../engine'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/hooks.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/hooks.test.tsx index aecb4cc154117..028a9af21311f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/hooks.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/hooks.test.tsx @@ -25,8 +25,9 @@ jest.mock('react', () => ({ })); import React from 'react'; -import { act } from 'react-dom/test-utils'; + import { mount, ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { useSearchContextState, useSearchContextActions } from './hooks'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.test.tsx index 5fe47d5942ab8..b55163ca9843a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; + // @ts-expect-error types are not available for this package yet import { Paging, ResultsPerPage } from '@elastic/react-search-ui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx index 846671c62de82..d81b056842642 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; // @ts-expect-error types are not available for this package yet import { Paging, ResultsPerPage } from '@elastic/react-search-ui'; + import { PagingView, ResultsPerPageView } from './views'; export const Pagination: React.FC<{ 'aria-label': string }> = ({ 'aria-label': ariaLabel }) => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx index b0dccf0583e2f..bfa5c8264fece 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx @@ -8,21 +8,24 @@ import '../../../../__mocks__/enterprise_search_url.mock'; import { setMockValues } from '../../../../__mocks__'; +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +// @ts-expect-error types are not available for this package yet +import { SearchProvider, Facet } from '@elastic/react-search-ui'; + jest.mock('../../../../shared/use_local_storage', () => ({ useLocalStorage: jest.fn(), })); import { useLocalStorage } from '../../../../shared/use_local_storage'; -import React from 'react'; -// @ts-expect-error types are not available for this package yet -import { SearchProvider, Facet } from '@elastic/react-search-ui'; -import { shallow, ShallowWrapper } from 'enzyme'; - import { CustomizationCallout } from './customization_callout'; import { CustomizationModal } from './customization_modal'; + import { Fields } from './types'; -import { SearchExperience } from './search_experience'; +import { SearchExperience } from './'; describe('SearchExperience', () => { const values = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx index 6ae4f264d7c74..6fbc6305edb25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx @@ -7,13 +7,14 @@ import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; import { useValues } from 'kea'; + import { EuiButton, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; // @ts-expect-error types are not available for this package yet; import { SearchProvider, SearchBox, Sorting, Facet } from '@elastic/react-search-ui'; // @ts-expect-error types are not available for this package yet import AppSearchAPIConnector from '@elastic/search-ui-app-search-connector'; +import { i18n } from '@kbn/i18n'; import './search_experience.scss'; @@ -21,14 +22,14 @@ import { externalUrl } from '../../../../shared/enterprise_search_url'; import { useLocalStorage } from '../../../../shared/use_local_storage'; import { EngineLogic } from '../../engine'; -import { Fields, SortOption } from './types'; -import { SearchBoxView, SortingView, MultiCheckboxFacetsView } from './views'; -import { SearchExperienceContent } from './search_experience_content'; import { buildSearchUIConfig } from './build_search_ui_config'; -import { CustomizationCallout } from './customization_callout'; -import { CustomizationModal } from './customization_modal'; import { buildSortOptions } from './build_sort_options'; import { ASCENDING, DESCENDING } from './constants'; +import { CustomizationCallout } from './customization_callout'; +import { CustomizationModal } from './customization_modal'; +import { SearchExperienceContent } from './search_experience_content'; +import { Fields, SortOption } from './types'; +import { SearchBoxView, SortingView, MultiCheckboxFacetsView } from './views'; const RECENTLY_UPLOADED = i18n.translate( 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx index 737e3ea1b2999..49f51c2010e3a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx @@ -6,18 +6,20 @@ */ import { setMockValues } from '../../../../__mocks__/kea.mock'; -import { setMockSearchContextState } from './__mocks__/hooks.mock'; import React from 'react'; import { shallow, mount } from 'enzyme'; + // @ts-expect-error types are not available for this package yet import { Results } from '@elastic/react-search-ui'; -import { ResultView } from './views'; -import { Pagination } from './pagination'; import { SchemaTypes } from '../../../../shared/types'; + +import { setMockSearchContextState } from './__mocks__/hooks.mock'; +import { Pagination } from './pagination'; import { SearchExperienceContent } from './search_experience_content'; +import { ResultView } from './views'; describe('SearchExperienceContent', () => { const searchState = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx index 45c20d8ffce2c..91db26ac676c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx @@ -7,20 +7,22 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; +import { useValues } from 'kea'; + import { EuiFlexGroup, EuiSpacer, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; // @ts-expect-error types are not available for this package yet import { Results, Paging, ResultsPerPage } from '@elastic/react-search-ui'; -import { useValues } from 'kea'; +import { i18n } from '@kbn/i18n'; -import { ResultView } from './views'; -import { Pagination } from './pagination'; -import { useSearchContextState } from './hooks'; -import { DocumentCreationButton } from '../document_creation_button'; import { AppLogic } from '../../../app_logic'; -import { EngineLogic } from '../../engine'; import { DOCS_PREFIX } from '../../../routes'; +import { EngineLogic } from '../../engine'; import { Result } from '../../result/types'; +import { DocumentCreationButton } from '../document_creation_button'; + +import { useSearchContextState } from './hooks'; +import { Pagination } from './pagination'; +import { ResultView } from './views'; export const SearchExperienceContent: React.FC = () => { const { resultSearchTerm, totalResults, wasSearched } = useSearchContextState(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/paging_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/paging_view.test.tsx index 03b9e33f89fef..28cd126e5c004 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/paging_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/paging_view.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; + import { EuiPagination } from '@elastic/eui'; import { PagingView } from './paging_view'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx index e06603894c288..24685aef71078 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx @@ -9,10 +9,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ResultView } from '.'; import { SchemaTypes } from '../../../../../shared/types'; import { Result } from '../../../result/result'; +import { ResultView } from '.'; + describe('ResultView', () => { const result = { id: { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx index 9dd3fcea5f754..b133780310a4c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx @@ -7,9 +7,9 @@ import React from 'react'; -import { Result as ResultType } from '../../../result/types'; import { Schema } from '../../../../../shared/types'; import { Result } from '../../../result/result'; +import { Result as ResultType } from '../../../result/types'; export interface Props { result: ResultType; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.test.tsx index 70e4d7e4e1878..24db762e26e32 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; + import { EuiSelect } from '@elastic/eui'; import { ResultsPerPageView } from '.'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.tsx index b57944042e67f..5056d56d1f3d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.tsx @@ -7,8 +7,8 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; const wrapResultsPerPageOptionForEuiSelect: (option: number) => EuiSelectOption = (option) => ({ text: option, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.test.tsx index 182e2ea222f90..a35fcefb30ac6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; + import { EuiFieldSearch } from '@elastic/eui'; import { SearchBoxView } from './search_box_view'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.test.tsx index 4f7317a2bf5d0..a147f45feef14 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; + import { EuiSelect } from '@elastic/eui'; import { SortingView } from '.'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx index 047caf6ca1e3b..e3f21b67a6530 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx @@ -7,8 +7,8 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; interface Option { label: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts index fbe08cbeb939f..664a3006cfa2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts @@ -10,6 +10,7 @@ import { kea, MakeLogicType } from 'kea'; import { HttpLogic } from '../../../shared/http'; import { IIndexingStatus } from '../../../shared/types'; + import { EngineDetails } from './types'; interface EngineValues { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx index 8ed36ad5ab006..1781883aa6532 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx @@ -9,7 +9,9 @@ import { setMockValues, rerender } from '../../../__mocks__'; import { mockEngineValues } from '../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiBadge, EuiIcon } from '@elastic/eui'; import { EngineNav } from './engine_nav'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index b1b31c245eb99..447e4d678bcdb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -6,11 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; import { EuiText, EuiBadge, EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; import { SideNavLink, SideNavItem } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; import { @@ -27,23 +29,23 @@ import { ENGINE_SEARCH_UI_PATH, ENGINE_API_LOGS_PATH, } from '../../routes'; -import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; -import { ENGINES_TITLE } from '../engines'; -import { OVERVIEW_TITLE } from '../engine_overview'; import { ANALYTICS_TITLE } from '../analytics'; -import { DOCUMENTS_TITLE } from '../documents'; -import { SCHEMA_TITLE } from '../schema'; +import { API_LOGS_TITLE } from '../api_logs'; import { CRAWLER_TITLE } from '../crawler'; -import { RELEVANCE_TUNING_TITLE } from '../relevance_tuning'; -import { SYNONYMS_TITLE } from '../synonyms'; import { CURATIONS_TITLE } from '../curations'; +import { DOCUMENTS_TITLE } from '../documents'; +import { OVERVIEW_TITLE } from '../engine_overview'; +import { ENGINES_TITLE } from '../engines'; +import { RELEVANCE_TUNING_TITLE } from '../relevance_tuning'; import { RESULT_SETTINGS_TITLE } from '../result_settings'; +import { SCHEMA_TITLE } from '../schema'; import { SEARCH_UI_TITLE } from '../search_ui'; -import { API_LOGS_TITLE } from '../api_logs'; +import { SYNONYMS_TITLE } from '../synonyms'; -import { EngineLogic, generateEnginePath } from './'; import { EngineDetails } from './types'; +import { EngineLogic, generateEnginePath } from './'; + import './engine_nav.scss'; export const EngineNav: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index cff05b296846b..3740882dee3db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -6,17 +6,18 @@ */ import '../../../__mocks__/react_router_history.mock'; -import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import { mockFlashMessageHelpers, setMockValues, setMockActions } from '../../../__mocks__'; +import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import { mockEngineValues } from '../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; import { Switch, Redirect, useParams } from 'react-router-dom'; +import { shallow } from 'enzyme'; + import { Loading } from '../../../shared/loading'; -import { EngineOverview } from '../engine_overview'; import { AnalyticsRouter } from '../analytics'; +import { EngineOverview } from '../engine_overview'; import { RelevanceTuning } from '../relevance_tuning'; import { EngineRouter } from './engine_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 257bb1e69ad7f..2f1c3bc57d331 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -7,12 +7,14 @@ import React, { useEffect } from 'react'; import { Route, Switch, Redirect, useParams } from 'react-router-dom'; + import { useValues, useActions } from 'kea'; import { i18n } from '@kbn/i18n'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { setQueuedErrorMessage } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; import { AppLogic } from '../../app_logic'; // TODO: Uncomment and add more routes as we migrate them @@ -31,13 +33,11 @@ import { // ENGINE_SEARCH_UI_PATH, // ENGINE_API_LOGS_PATH, } from '../../routes'; -import { ENGINES_TITLE } from '../engines'; -import { OVERVIEW_TITLE } from '../engine_overview'; - -import { Loading } from '../../../shared/loading'; -import { EngineOverview } from '../engine_overview'; import { AnalyticsRouter } from '../analytics'; import { DocumentDetail, Documents } from '../documents'; +import { OVERVIEW_TITLE } from '../engine_overview'; +import { EngineOverview } from '../engine_overview'; +import { ENGINES_TITLE } from '../engines'; import { RelevanceTuning } from '../relevance_tuning'; import { EngineLogic } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts index d20f9890cd4db..b50e8eb555dc9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ApiToken } from '../credentials/types'; import { Schema, SchemaConflicts, IIndexingStatus } from '../../../shared/types'; +import { ApiToken } from '../credentials/types'; export interface Engine { name: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx index 7b52a04d07958..42fa9777563db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx @@ -8,6 +8,7 @@ import '../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx index d7290533f4f7b..625ba2e905840 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx @@ -17,9 +17,9 @@ import { import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { ENGINE_API_LOGS_PATH } from '../../../routes'; +import { RECENT_API_EVENTS } from '../../api_logs/constants'; import { generateEnginePath } from '../../engine'; -import { RECENT_API_EVENTS } from '../../api_logs/constants'; import { VIEW_API_LOGS } from '../constants'; export const RecentApiLogs: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx index 867b78f859a22..a2f35b4709939 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx @@ -9,6 +9,7 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx index 4fa2246ee6170..6bd973ae142a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { useValues } from 'kea'; import { @@ -21,12 +22,12 @@ import { import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { ENGINE_ANALYTICS_PATH, ENGINE_API_LOGS_PATH } from '../../../routes'; +import { AnalyticsChart, convertToChartData } from '../../analytics'; +import { TOTAL_QUERIES, TOTAL_API_OPERATIONS } from '../../analytics/constants'; import { generateEnginePath } from '../../engine'; -import { TOTAL_QUERIES, TOTAL_API_OPERATIONS } from '../../analytics/constants'; import { VIEW_ANALYTICS, VIEW_API_LOGS, LAST_7_DAYS } from '../constants'; -import { AnalyticsChart, convertToChartData } from '../../analytics'; -import { EngineOverviewLogic } from '../'; +import { EngineOverviewLogic } from '../index'; export const TotalCharts: React.FC = () => { const { startDate, queriesPerDay, operationsPerDay } = useValues(EngineOverviewLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx index a897c635eeadd..7fcda61073c5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsCards } from '../../analytics'; + import { TotalStats } from './total_stats'; describe('TotalStats', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx index 3eb208fa86504..35c6fa439a416 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx @@ -6,12 +6,12 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { TOTAL_QUERIES, TOTAL_DOCUMENTS, TOTAL_CLICKS } from '../../analytics/constants'; import { AnalyticsCards } from '../../analytics'; - -import { EngineOverviewLogic } from '../'; +import { TOTAL_QUERIES, TOTAL_DOCUMENTS, TOTAL_CLICKS } from '../../analytics/constants'; +import { EngineOverviewLogic } from '../index'; export const TotalStats: React.FC = () => { const { totalQueries, documentCount, totalClicks } = useValues(EngineOverviewLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx index 7cd042a646e73..4c61a713b3793 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiEmptyPrompt } from '@elastic/eui'; import { UnavailablePrompt } from './unavailable_prompt'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx index 2916be92ead99..69e79ecfc580d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx @@ -7,8 +7,8 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; export const UnavailablePrompt: React.FC = () => ( { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index 9e673d48a7e5b..77552b36af239 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -6,16 +6,19 @@ */ import React, { useEffect } from 'react'; + import { useActions, useValues } from 'kea'; +import { Loading } from '../../../shared/loading'; import { AppLogic } from '../../app_logic'; import { EngineLogic } from '../engine'; -import { Loading } from '../../../shared/loading'; -import { EngineOverviewLogic } from './'; import { EmptyEngineOverview } from './engine_overview_empty'; + import { EngineOverviewMetrics } from './engine_overview_metrics'; +import { EngineOverviewLogic } from './'; + export const EngineOverview: React.FC = () => { const { myRole: { canManageEngineDocuments, canViewEngineCredentials }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx index 5947618e59c16..9066283229a04 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx @@ -6,12 +6,15 @@ */ import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiButton } from '@elastic/eui'; import { docLinks } from '../../../shared/doc_links'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; + import { EmptyEngineOverview } from './engine_overview_empty'; describe('EmptyEngineOverview', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx index 6a0c46286907d..81bf3716edfb8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx @@ -7,7 +7,6 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiPageHeader, EuiPageHeaderSection, @@ -15,6 +14,7 @@ import { EuiTitle, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DOCS_PREFIX } from '../../routes'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx index ebcdbaf1f7f09..638c8b0da87ce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx @@ -8,6 +8,7 @@ import { setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { UnavailablePrompt, TotalStats, TotalCharts, RecentApiLogs } from './components'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx index ffb1a25d21cae..34a154ca83741 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx @@ -6,15 +6,16 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiPageHeader, EuiTitle, EuiSpacer } from '@elastic/eui'; - -import { EngineOverviewLogic } from './'; +import { i18n } from '@kbn/i18n'; import { UnavailablePrompt, TotalStats, TotalCharts, RecentApiLogs } from './components'; +import { EngineOverviewLogic } from './'; + export const EngineOverviewMetrics: React.FC = () => { const { apiLogsUnavailable } = useValues(EngineOverviewLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx index 9c2818f9907a4..33ca5bd8248c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EngineIcon } from './engine_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx index b04226b6b1dfb..ac540eec3ff91 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx @@ -9,7 +9,9 @@ import '../../../../__mocks__/kea.mock'; import { mockTelemetryActions } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { EmptyState } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx index 60a454a5707c9..5419a175c9eff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx @@ -6,13 +6,15 @@ */ import React from 'react'; + import { useActions } from 'kea'; + import { EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TelemetryLogic } from '../../../../shared/telemetry'; import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; +import { TelemetryLogic } from '../../../../shared/telemetry'; import { CREATE_ENGINES_PATH } from '../../../routes'; import { EnginesOverviewHeader } from './header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx index 6dedb90690ace..5ccd2c552ef02 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx @@ -10,6 +10,7 @@ import '../../../../__mocks__/enterprise_search_url.mock'; import { mockTelemetryActions } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EnginesOverviewHeader } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx index 8a8227821b492..290270c08258c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useActions } from 'kea'; + import { EuiPageHeader, EuiPageHeaderSection, @@ -17,8 +19,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TelemetryLogic } from '../../../../shared/telemetry'; import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; +import { TelemetryLogic } from '../../../../shared/telemetry'; export const EnginesOverviewHeader: React.FC = () => { const { sendAppSearchTelemetry } = useActions(TelemetryLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx index 4adc8c11fa0dd..f7ccfea4bb4d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiLoadingContent } from '@elastic/eui'; import { LoadingState } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx index 48160602106cd..155d8263c484d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; + import { EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; + import { EnginesOverviewHeader } from './header'; export const LoadingState: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts index c25f60e47598e..9e9bfc4973124 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts @@ -10,6 +10,7 @@ import { LogicMounter, mockHttpValues } from '../../../__mocks__'; import { nextTick } from '@kbn/test/jest'; import { EngineDetails } from '../engine/types'; + import { EnginesLogic } from './'; describe('EnginesLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx index f4aeb60a88250..cdc06dbbe3921 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx @@ -9,6 +9,7 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions, rerender } from '../../../__mocks__'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; import { LoadingState, EmptyState } from './components'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index c13db688fc2b6..2835c8b7cb3c4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -6,7 +6,9 @@ */ import React, { useEffect } from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiPageContent, EuiPageContentHeader, @@ -15,17 +17,17 @@ import { EuiSpacer, } from '@elastic/eui'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { LicensingLogic } from '../../../shared/licensing'; +import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { EngineIcon } from './assets/engine_icon'; import { MetaEngineIcon } from './assets/meta_engine_icon'; -import { ENGINES_TITLE, META_ENGINES_TITLE } from './constants'; import { EnginesOverviewHeader, LoadingState, EmptyState } from './components'; -import { EnginesTable } from './engines_table'; +import { ENGINES_TITLE, META_ENGINES_TITLE } from './constants'; import { EnginesLogic } from './engines_logic'; +import { EnginesTable } from './engines_table'; import './engines_overview.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx index 65b96035eaaee..d6f0946164ea4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx @@ -9,10 +9,13 @@ import '../../../__mocks__/enterprise_search_url.mock'; import { mockTelemetryActions, mountWithIntl } from '../../../__mocks__'; import React from 'react'; + import { EuiBasicTable, EuiPagination, EuiButtonEmpty } from '@elastic/eui'; + import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { EngineDetails } from '../engine/types'; + import { EnginesTable } from './engines_table'; describe('EnginesTable', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx index b439d7e6bdf33..d41c5c908c08f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx @@ -6,18 +6,19 @@ */ import React from 'react'; + import { useActions } from 'kea'; + import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; -import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; - -import { TelemetryLogic } from '../../../shared/telemetry'; -import { EuiLinkTo } from '../../../shared/react_router_helpers'; -import { generateEncodedPath } from '../../utils/encode_path_params'; -import { ENGINE_PATH } from '../../routes'; +import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; +import { EuiLinkTo } from '../../../shared/react_router_helpers'; +import { TelemetryLogic } from '../../../shared/telemetry'; import { UNIVERSAL_LANGUAGE } from '../../constants'; +import { ENGINE_PATH } from '../../routes'; +import { generateEncodedPath } from '../../utils/encode_path_params'; import { EngineDetails } from '../engine/types'; interface EnginesTablePagination { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx index 6ff33385df9a5..9ec3fdda63656 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ErrorStatePrompt } from '../../../shared/error_state'; + import { ErrorConnecting } from './'; describe('ErrorConnecting', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx index ad5eff6c4dacf..d7fde0cd5dd25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiPageContent } from '@elastic/eui'; import { ErrorStatePrompt } from '../../../shared/error_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index 2d39b5a9aa05c..f76ad78c847d1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import React from 'react'; + import { EuiSpacer, EuiPageHeader, @@ -13,7 +15,6 @@ import { EuiPageContentBody, EuiPageContent, } from '@elastic/eui'; -import React from 'react'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { Schema } from '../../../shared/types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx index d0bc1c9a88c5f..124edb6871453 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx @@ -6,14 +6,16 @@ */ import '../../../../__mocks__/shallow_useeffect.mock'; -import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; -import { mountWithIntl } from '../../../../__mocks__'; +import { setMockValues, setMockActions, mountWithIntl } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCallOut, EuiLink } from '@elastic/eui'; import { LogRetentionOptions } from '../'; + import { LogRetentionCallout } from './'; describe('LogRetentionCallout', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx index 0252a788f75ef..235d977793161 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx @@ -6,11 +6,12 @@ */ import React, { useEffect } from 'react'; + import { useValues, useActions } from 'kea'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { EuiLinkTo } from '../../../../shared/react_router_helpers'; @@ -19,7 +20,7 @@ import { SETTINGS_PATH } from '../../../routes'; import { ANALYTICS_TITLE } from '../../analytics'; import { API_LOGS_TITLE } from '../../api_logs'; -import { LogRetentionLogic, LogRetentionOptions, renderLogRetentionDate } from '../'; +import { LogRetentionLogic, LogRetentionOptions, renderLogRetentionDate } from '../index'; const TITLE_MAP = { [LogRetentionOptions.Analytics]: ANALYTICS_TITLE, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx index 14615f6ac2dd9..854a9f1d8d162 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx @@ -9,10 +9,13 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow, mount } from 'enzyme'; + import { EuiIconTip } from '@elastic/eui'; import { LogRetentionOptions, LogRetentionMessage } from '../'; + import { LogRetentionTooltip } from './'; describe('LogRetentionTooltip', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx index e3b428baa6d9a..bf074ba0272f2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx @@ -6,10 +6,11 @@ */ import React, { useEffect } from 'react'; + import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiIconTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { LogRetentionLogic, LogRetentionMessage, LogRetentionOptions } from '../'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts index 9615aba5fdef4..19bd2af50aad9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts @@ -10,7 +10,8 @@ import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../ import { nextTick } from '@kbn/test/jest'; import { LogRetentionOptions } from './types'; -import { LogRetentionLogic } from './log_retention_logic'; + +import { LogRetentionLogic } from './'; describe('LogRetentionLogic', () => { const { mount } = new LogicMounter(LogRetentionLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts index 77d4cf395196a..ec078842dab55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts @@ -7,8 +7,8 @@ import { kea, MakeLogicType } from 'kea'; -import { HttpLogic } from '../../../shared/http'; import { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; import { LogRetentionOptions, LogRetention, LogRetentionServer } from './types'; import { convertLogRetentionFromServerToClient } from './utils/convert_log_retention'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx index 0f231092a36e2..c7c4d90d91ce8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; -import { FormattedDate, FormattedMessage } from '@kbn/i18n/react'; + import { i18n } from '@kbn/i18n'; +import { FormattedDate, FormattedMessage } from '@kbn/i18n/react'; import { LogRetentionOptions, LogRetentionSettings, LogRetentionPolicy } from '../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx index be95261a35c25..cd71e37108927 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import { setMockValues } from '../../../../__mocks__/kea.mock'; -import { mountWithIntl } from '../../../../__mocks__'; +import { setMockValues, mountWithIntl } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { LogRetentionOptions } from '../types'; + import { LogRetentionMessage } from './'; describe('LogRetentionMessage', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx index 62bac44b122af..7d34a2567ba14 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { useValues } from 'kea'; import { AppLogic } from '../../../app_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx index 56e31ec6bf970..83e83c0f9ea43 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiPageHeader, EuiPageHeaderSection, @@ -14,8 +15,8 @@ import { EuiPageContent, } from '@elastic/eui'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { RELEVANCE_TUNING_TITLE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index 586a845ce382a..7f7bce1b7ba95 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -9,7 +9,7 @@ import { LogicMounter } from '../../../__mocks__'; import { BoostType } from './types'; -import { RelevanceTuningLogic } from './relevance_tuning_logic'; +import { RelevanceTuningLogic } from './'; describe('RelevanceTuningLogic', () => { const { mount } = new LogicMounter(RelevanceTuningLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx index 0c3749d1ccb3d..41428999b1e40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx @@ -6,15 +6,17 @@ */ import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiPanel } from '@elastic/eui'; -import { ResultField } from './result_field'; -import { ResultHeader } from './result_header'; import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components'; import { SchemaTypes } from '../../../shared/types'; import { Result } from './result'; +import { ResultField } from './result_field'; +import { ResultHeader } from './result_header'; describe('Result', () => { const props = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx index d84b079ea9d72..7288fdf39f3ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx @@ -6,6 +6,7 @@ */ import React, { useState, useMemo } from 'react'; + import classNames from 'classnames'; import './result.scss'; @@ -14,13 +15,14 @@ import { EuiPanel, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components'; -import { generateEncodedPath } from '../../utils/encode_path_params'; -import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes'; import { Schema } from '../../../shared/types'; -import { FieldValue, Result as ResultType } from './types'; +import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes'; +import { generateEncodedPath } from '../../utils/encode_path_params'; + import { ResultField } from './result_field'; import { ResultHeader } from './result_header'; +import { FieldValue, Result as ResultType } from './types'; interface Props { result: ResultType; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx index 6869708627b8d..1e79266dd7e7d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ResultField } from './result_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx index a1c3ccd93622a..003810ec40a8d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; -import { ResultFieldValue } from '.'; + import { FieldType, Raw, Snippet } from './types'; +import { ResultFieldValue } from '.'; + import './result_field.scss'; interface Props { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx index e1cefa1d79469..c732c9c8216c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; import { ResultFieldValue } from '.'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx index 9d90b3ae35a8f..dcefd0f6bc0b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ResultHeader } from './result_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx index e8cc8796440a9..52fa81943bb2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { mount } from 'enzyme'; import { ResultHeaderItem } from './result_header_item'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.test.tsx index 3e450b7a7bb70..8477f0e8e2ce2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { GenericConfirmationModal } from './generic_confirmation_modal'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx index b792ace4dac3f..eb64fe6421d80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx @@ -6,7 +6,6 @@ */ import React, { ReactNode, useState } from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiButton, @@ -21,6 +20,7 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; interface GenericConfirmationModalProps { description: ReactNode; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx index a6d0cab532729..494517a438372 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx @@ -8,9 +8,11 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { LogRetentionOptions } from '../../log_retention'; + import { GenericConfirmationModal } from './generic_confirmation_modal'; import { LogRetentionConfirmationModal } from './log_retention_confirmation_modal'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx index 52a2478d7158e..ca1fa9a8d0737 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiTextColor, EuiOverlayMask } from '@elastic/eui'; import { useActions, useValues } from 'kea'; +import { EuiTextColor, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { LogRetentionLogic, LogRetentionOptions } from '../../log_retention'; + import { GenericConfirmationModal } from './generic_confirmation_modal'; export const LogRetentionConfirmationModal: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx index 882b82979a511..aee23e61e76fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx @@ -9,9 +9,11 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { LogRetention } from '../../log_retention/types'; + import { LogRetentionPanel } from './log_retention_panel'; describe('', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx index 3a40be9efd5db..76fdcdac58ad4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -6,11 +6,12 @@ */ import React, { useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; import { useActions, useValues } from 'kea'; +import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { DOCS_PREFIX } from '../../../routes'; import { LogRetentionLogic, LogRetentionOptions, LogRetentionMessage } from '../../log_retention'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx index fead8cda0c0e2..41d446b8e36fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiPageContentBody } from '@elastic/eui'; import { Settings } from './settings'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx index c029cf344f18b..510075eba4abf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx @@ -15,10 +15,11 @@ import { EuiTitle, } from '@elastic/eui'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { LogRetentionPanel, LogRetentionConfirmationModal } from './log_retention'; + import { SETTINGS_TITLE } from './'; export const Settings: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx index e8dcb6ff98358..0b4a86870a69d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx @@ -6,10 +6,12 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout } from '../../../shared/setup_guide'; + import { SetupGuide } from './'; describe('SetupGuide', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index befb06c719a39..3d96b22859fad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -6,15 +6,17 @@ */ import React from 'react'; + import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { DOCS_PREFIX } from '../../routes'; + import GettingStarted from './assets/getting_started.png'; export const SetupGuide: React.FC = () => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index dc3c0b03148d9..0e8220266d613 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -11,13 +11,16 @@ import { setMockValues, setMockActions } from '../__mocks__'; import React from 'react'; import { Redirect } from 'react-router-dom'; + import { shallow } from 'enzyme'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; -import { SetupGuide } from './components/setup_guide'; -import { ErrorConnecting } from './components/error_connecting'; -import { EnginesOverview } from './components/engines'; + import { EngineRouter } from './components/engine'; +import { EnginesOverview } from './components/engines'; +import { ErrorConnecting } from './components/error_connecting'; +import { SetupGuide } from './components/setup_guide'; + import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; describe('AppSearch', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 918697422af6b..36ac3fb4dbc5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -7,18 +7,26 @@ import React, { useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; + import { useActions, useValues } from 'kea'; +import { APP_SEARCH_PLUGIN } from '../../../common/constants'; +import { InitialAppData } from '../../../common/types'; import { getAppSearchUrl } from '../shared/enterprise_search_url'; -import { KibanaLogic } from '../shared/kibana'; import { HttpLogic } from '../shared/http'; -import { AppLogic } from './app_logic'; -import { InitialAppData } from '../../../common/types'; - -import { APP_SEARCH_PLUGIN } from '../../../common/constants'; +import { KibanaLogic } from '../shared/kibana'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; -import { EngineNav, EngineRouter } from './components/engine'; +import { NotFound } from '../shared/not_found'; +import { AppLogic } from './app_logic'; +import { Credentials, CREDENTIALS_TITLE } from './components/credentials'; +import { EngineNav, EngineRouter } from './components/engine'; +import { EnginesOverview, ENGINES_TITLE } from './components/engines'; +import { ErrorConnecting } from './components/error_connecting'; +import { Library } from './components/library'; +import { ROLE_MAPPINGS_TITLE } from './components/role_mappings'; +import { Settings, SETTINGS_TITLE } from './components/settings'; +import { SetupGuide } from './components/setup_guide'; import { ROOT_PATH, SETUP_GUIDE_PATH, @@ -30,15 +38,6 @@ import { LIBRARY_PATH, } from './routes'; -import { SetupGuide } from './components/setup_guide'; -import { ErrorConnecting } from './components/error_connecting'; -import { NotFound } from '../shared/not_found'; -import { EnginesOverview, ENGINES_TITLE } from './components/engines'; -import { Settings, SETTINGS_TITLE } from './components/settings'; -import { Credentials, CREDENTIALS_TITLE } from './components/credentials'; -import { ROLE_MAPPINGS_TITLE } from './components/role_mappings'; -import { Library } from './components/library'; - export const AppSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); return !config.host ? : ; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx index 6ff33385df9a5..9ec3fdda63656 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ErrorStatePrompt } from '../../../shared/error_state'; + import { ErrorConnecting } from './'; describe('ErrorConnecting', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx index cb1abc275d37f..afee20df106e8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; + import { EuiPage, EuiPageContent } from '@elastic/eui'; -import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { ErrorStatePrompt } from '../../../shared/error_state'; +import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; export const ErrorConnecting: React.FC = () => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx index a9098689b3d0e..8631e6e2a51d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -8,11 +8,13 @@ import { setMockValues, mockTelemetryActions } from '../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCard } from '@elastic/eui'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; + import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { ProductCard } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index d31daeef54de9..20727e37460f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -6,14 +6,16 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; import { snakeCase } from 'lodash'; -import { i18n } from '@kbn/i18n'; + import { EuiCard, EuiTextColor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { KibanaLogic } from '../../../shared/kibana'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../shared/telemetry'; -import { KibanaLogic } from '../../../shared/kibana'; import './product_card.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx index 0d55e2ce21c74..9ee34634e3797 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx @@ -8,11 +8,13 @@ import { setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiPage } from '@elastic/eui'; -import { SetupGuideCta } from '../setup_guide'; import { ProductCard } from '../product_card'; +import { SetupGuideCta } from '../setup_guide'; import { ProductSelector } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index 910840f023bb2..f2476a5770c25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues } from 'kea'; + import { EuiPage, EuiPageBody, @@ -25,11 +27,10 @@ import { KibanaLogic } from '../../../shared/kibana'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { ProductCard } from '../product_card'; -import { SetupGuideCta } from '../setup_guide'; - import AppSearchImage from '../../assets/app_search.png'; import WorkplaceSearchImage from '../../assets/workplace_search.png'; +import { ProductCard } from '../product_card'; +import { SetupGuideCta } from '../setup_guide'; interface ProductSelectorProps { access: { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx index e2f3595d26974..44f06de7ff137 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx @@ -6,10 +6,12 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout } from '../../../shared/setup_guide'; + import { SetupGuide } from './'; describe('SetupGuide', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx index 02327e01c5ede..c59742d7ccbea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx @@ -6,14 +6,16 @@ */ import React from 'react'; + import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; + import GettingStarted from './assets/getting_started.png'; export const SetupGuide: React.FC = () => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx index 140e779df55d2..659af6d23c6d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SetupGuideCta } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx index 7d32b11ba7ae8..17260cc15793a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx @@ -6,8 +6,10 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; + import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { EuiPanelTo } from '../../../shared/react_router_helpers'; import CtaImage from './assets/getting_started.png'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index e4508f4e99276..2d8dbd55f4366 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -5,15 +5,17 @@ * 2.0. */ +import { setMockValues, rerender } from '../__mocks__'; + import React from 'react'; -import { shallow } from 'enzyme'; -import { setMockValues, rerender } from '../__mocks__'; +import { shallow } from 'enzyme'; -import { EnterpriseSearch } from './'; -import { SetupGuide } from './components/setup_guide'; import { ErrorConnecting } from './components/error_connecting'; import { ProductSelector } from './components/product_selector'; +import { SetupGuide } from './components/setup_guide'; + +import { EnterpriseSearch } from './'; describe('EnterpriseSearch', () => { it('renders the Setup Guide and Product Selector', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index 50ed0bce75cf8..b21e46429672a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -7,18 +7,17 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; + import { useValues } from 'kea'; -import { KibanaLogic } from '../shared/kibana'; import { InitialAppData } from '../../../common/types'; - import { HttpLogic } from '../shared/http'; - -import { ROOT_PATH, SETUP_GUIDE_PATH } from './routes'; +import { KibanaLogic } from '../shared/kibana'; import { ErrorConnecting } from './components/error_connecting'; import { ProductSelector } from './components/product_selector'; import { SetupGuide } from './components/setup_guide'; +import { ROOT_PATH, SETUP_GUIDE_PATH } from './routes'; import './index.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index f36561787eb69..2e0940b9c4af2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -6,17 +6,19 @@ */ import React from 'react'; + import { getContext } from 'kea'; -import { coreMock } from 'src/core/public/mocks'; -import { licensingMock } from '../../../licensing/public/mocks'; +import { coreMock } from '../../../../../src/core/public/mocks'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { licensingMock } from '../../../licensing/public/mocks'; -import { renderApp, renderHeaderActions } from './'; -import { EnterpriseSearch } from './enterprise_search'; import { AppSearch } from './app_search'; -import { WorkplaceSearch } from './workplace_search'; +import { EnterpriseSearch } from './enterprise_search'; import { KibanaLogic } from './shared/kibana'; +import { WorkplaceSearch } from './workplace_search'; + +import { renderApp, renderHeaderActions } from './'; describe('renderApp', () => { const kibanaDeps = { diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 97e43f758e5b8..155ff5b92ba27 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -7,21 +7,23 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Router } from 'react-router-dom'; import { Provider } from 'react-redux'; -import { Store } from 'redux'; +import { Router } from 'react-router-dom'; + import { getContext, resetContext } from 'kea'; +import { Store } from 'redux'; + import { I18nProvider } from '@kbn/i18n/react'; -import { AppMountParameters, CoreStart } from 'src/core/public'; -import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; +import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; import { InitialAppData } from '../../common/types'; +import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; +import { externalUrl } from './shared/enterprise_search_url'; +import { mountFlashMessagesLogic } from './shared/flash_messages'; +import { mountHttpLogic } from './shared/http'; import { mountKibanaLogic } from './shared/kibana'; import { mountLicensingLogic } from './shared/licensing'; -import { mountHttpLogic } from './shared/http'; -import { mountFlashMessagesLogic } from './shared/flash_messages'; -import { externalUrl } from './shared/enterprise_search_url'; /** * This file serves as a reusable wrapper to share Kibana-level context and other helpers diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx index 9870264eb1c2f..d9d31f5a45d4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx @@ -8,7 +8,9 @@ import '../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiEmptyPrompt } from '@elastic/eui'; import { ErrorStatePrompt } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index cf8b234442002..f855c7b67dc6e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; + import { useValues } from 'kea'; + import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButtonTo } from '../react_router_helpers'; import { KibanaLogic } from '../../shared/kibana'; +import { EuiButtonTo } from '../react_router_helpers'; import './error_state_prompt.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx index 2a31c0ecd66a8..aa45ce58af86a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx @@ -8,7 +8,9 @@ import { setMockValues } from '../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCallOut } from '@elastic/eui'; import { FlashMessages } from './flash_messages'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx index 5f38961b8a341..60d80487a2593 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx @@ -6,7 +6,9 @@ */ import React, { Fragment } from 'react'; + import { useValues } from 'kea'; + import { EuiCallOut, EuiCallOutProps, EuiSpacer } from '@elastic/eui'; import { FlashMessagesLogic } from './flash_messages_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts index 61f667719e3e6..7fc78c99fb242 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { mockKibanaValues } from '../../__mocks__/kibana_logic.mock'; + import { resetContext } from 'kea'; -import { mockKibanaValues } from '../../__mocks__/kibana_logic.mock'; const { history } = mockKibanaValues; -import { FlashMessagesLogic, mountFlashMessagesLogic, IFlashMessage } from './flash_messages_logic'; +import { FlashMessagesLogic, mountFlashMessagesLogic } from './flash_messages_logic'; +import { IFlashMessage } from './types'; describe('FlashMessagesLogic', () => { const mount = () => mountFlashMessagesLogic(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 26af4103aada1..5993e67b28a39 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -6,15 +6,10 @@ */ import { kea, MakeLogicType } from 'kea'; -import { ReactNode } from 'react'; import { KibanaLogic } from '../kibana'; -export interface IFlashMessage { - type: 'success' | 'info' | 'warning' | 'error'; - message: ReactNode; - description?: ReactNode; -} +import { IFlashMessage } from './types'; interface FlashMessagesValues { messages: IFlashMessage[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts index 1df1c6a7a680e..b6b0e23ce7d6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts @@ -8,6 +8,7 @@ import '../../__mocks__/kibana_logic.mock'; import { FlashMessagesLogic } from './flash_messages_logic'; + import { flashAPIErrors } from './handle_api_errors'; describe('flashAPIErrors', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts index 5fb824ebde9a0..11003d0fcc171 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts @@ -7,7 +7,8 @@ import { HttpResponse } from 'src/core/public'; -import { FlashMessagesLogic, IFlashMessage } from './flash_messages_logic'; +import { FlashMessagesLogic } from './flash_messages_logic'; +import { IFlashMessage } from './types'; /** * The API errors we are handling can come from one of two ways: diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts index 8d3605a19c22c..40317eb390547 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -6,7 +6,8 @@ */ export { FlashMessages } from './flash_messages'; -export { FlashMessagesLogic, IFlashMessage, mountFlashMessagesLogic } from './flash_messages_logic'; +export { FlashMessagesLogic, mountFlashMessagesLogic } from './flash_messages_logic'; +export { IFlashMessage } from './types'; export { flashAPIErrors } from './handle_api_errors'; export { setSuccessMessage, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts new file mode 100644 index 0000000000000..c1d2f8420198d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ReactNode } from 'react'; + +export interface IFlashMessage { + type: 'success' | 'info' | 'warning' | 'error'; + message: ReactNode; + description?: ReactNode; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx index 1888edca53034..af63b9a801edd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { HiddenText } from '.'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx index 5503baf0bdae4..35901496c5fbd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx @@ -6,6 +6,7 @@ */ import React, { useState, ReactElement } from 'react'; + import { i18n } from '@kbn/i18n'; interface ChildrenProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx index d8f02be60ef92..44bd8b78320d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx @@ -9,13 +9,14 @@ import '../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiPanel } from '@elastic/eui'; +import { IndexingStatus } from './indexing_status'; import { IndexingStatusContent } from './indexing_status_content'; import { IndexingStatusErrors } from './indexing_status_errors'; -import { IndexingStatus } from './indexing_status'; describe('IndexingStatus', () => { const getItemDetailPath = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx index 3898eda126415..ee0557e15396c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx @@ -11,12 +11,12 @@ import { useValues, useActions } from 'kea'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { IIndexingStatus } from '../types'; + import { IndexingStatusContent } from './indexing_status_content'; import { IndexingStatusErrors } from './indexing_status_errors'; import { IndexingStatusLogic } from './indexing_status_logic'; -import { IIndexingStatus } from '../types'; - export interface IIndexingStatusProps { viewLinkPath: string; itemId: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx index a744ddf8b5290..8998e640d6c35 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiProgress, EuiTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx index 3747fe020af20..eb5fa9d70f026 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts index 11ba1304d0a22..a436b669bcbe5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts @@ -7,9 +7,9 @@ import { kea, MakeLogicType } from 'kea'; +import { flashAPIErrors } from '../flash_messages'; import { HttpLogic } from '../http'; import { IIndexingStatus } from '../types'; -import { flashAPIErrors } from '../flash_messages'; interface IndexingStatusProps { statusPath: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts index 10b550fc93eb3..a5f54d16b2fad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { resetContext } from 'kea'; - import { mockKibanaValues } from '../../__mocks__'; +import { resetContext } from 'kea'; + import { KibanaLogic, mountKibanaLogic } from './kibana_logic'; describe('KibanaLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 4bb1859df09ea..8015d22f7c44a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { kea, MakeLogicType } from 'kea'; - import { FC } from 'react'; + import { History } from 'history'; -import { ApplicationStart, ChromeBreadcrumb } from 'src/core/public'; -import { ChartsPluginStart } from 'src/plugins/charts/public'; +import { kea, MakeLogicType } from 'kea'; + +import { ApplicationStart, ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { ChartsPluginStart } from '../../../../../../../src/plugins/charts/public'; import { CloudSetup } from '../../../../../cloud/public'; import { HttpLogic } from '../http'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index 64f53c767b17c..908cc0601ab9c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -6,10 +6,8 @@ */ import { useValues } from 'kea'; -import { EuiBreadcrumb } from '@elastic/eui'; -import { KibanaLogic } from '../kibana'; -import { HttpLogic } from '../http'; +import { EuiBreadcrumb } from '@elastic/eui'; import { ENTERPRISE_SEARCH_PLUGIN, @@ -18,6 +16,8 @@ import { } from '../../../../common/constants'; import { stripLeadingSlash } from '../../../../common/strip_slashes'; +import { HttpLogic } from '../http'; +import { KibanaLogic } from '../kibana'; import { letBrowserHandleEvent, createHref } from '../react_router_helpers'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx index 5b1aa64c42d64..c9743e6824018 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx @@ -9,6 +9,7 @@ import '../../__mocks__/shallow_useeffect.mock'; import { setMockValues, mockKibanaValues, mockHistory } from '../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; jest.mock('./generate_breadcrumbs', () => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx index fa127566b1b02..e639f9d22fb4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx @@ -6,6 +6,7 @@ */ import React, { useEffect } from 'react'; + import { useValues } from 'kea'; import { KibanaLogic } from '../kibana'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx index c67518e977de2..28092f75cdede 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiPageSideBar, EuiButton, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { Layout, INavContext } from './layout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx index 1af85905e6ccb..9cf5fccddbd5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx @@ -6,6 +6,7 @@ */ import React, { useState } from 'react'; + import classNames from 'classnames'; import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton, EuiCallOut } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx index ceb5f21ce056f..451b49738029d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx @@ -9,11 +9,13 @@ import '../../__mocks__/react_router_history.mock'; import React from 'react'; import { useLocation } from 'react-router-dom'; + import { shallow } from 'enzyme'; import { EuiLink } from '@elastic/eui'; -import { EuiLinkTo } from '../react_router_helpers'; + import { ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN } from '../../../../common/constants'; +import { EuiLinkTo } from '../react_router_helpers'; import { SideNav, SideNavLink, SideNavItem } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx index 605d3940a8cc7..58a5c7bbb229f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx @@ -7,14 +7,15 @@ import React, { useContext } from 'react'; import { useLocation } from 'react-router-dom'; + import classNames from 'classnames'; -import { i18n } from '@kbn/i18n'; import { EuiIcon, EuiTitle, EuiText, EuiLink } from '@elastic/eui'; // TODO: Remove EuiLink after full Kibana transition -import { EuiLinkTo } from '../react_router_helpers'; +import { i18n } from '@kbn/i18n'; import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../common/constants'; import { stripTrailingSlash } from '../../../../common/strip_slashes'; +import { EuiLinkTo } from '../react_router_helpers'; import { NavContext, INavContext } from './layout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx index c443467d5cb32..eab5694a27968 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiLoadingSpinner } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx index cfcbeaee72095..27a4dfdec0c07 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiLoadingSpinner } from '@elastic/eui'; import './loading.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx index 0bda848bc8d6c..7e75b2b47bb7a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx @@ -8,12 +8,14 @@ import { setMockValues } from '../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../common/constants'; import { SetAppSearchChrome } from '../kibana_chrome'; + import { AppSearchLogo } from './assets/app_search_logo'; import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx index 6102987464f55..5699568c40558 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; + import { EuiPageContent, EuiEmptyPrompt, @@ -16,6 +17,7 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { APP_SEARCH_PLUGIN, @@ -23,11 +25,11 @@ import { LICENSED_SUPPORT_URL, } from '../../../../common/constants'; -import { EuiButtonTo } from '../react_router_helpers'; -import { BreadcrumbTrail } from '../kibana_chrome/generate_breadcrumbs'; import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome'; -import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; +import { BreadcrumbTrail } from '../kibana_chrome/generate_breadcrumbs'; import { LicensingLogic } from '../licensing'; +import { EuiButtonTo } from '../react_router_helpers'; +import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; import { AppSearchLogo } from './assets/app_search_logo'; import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.test.ts index 0619dab19e2bd..fe2973cfdee32 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.test.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { httpServiceMock } from 'src/core/public/mocks'; import { mockHistory } from '../../__mocks__'; +import { httpServiceMock } from 'src/core/public/mocks'; + import { createHref } from './'; describe('createHref', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts index e36a65c2457db..ea28fc4d440c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts @@ -6,6 +6,7 @@ */ import { History } from 'history'; + import { HttpSetup } from 'src/core/public'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx index 4de43ce997b48..75639ffeb9d6b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx @@ -7,11 +7,13 @@ import '../../__mocks__/kea.mock'; +import { mockKibanaValues, mockHistory } from '../../__mocks__'; + import React from 'react'; + import { shallow, mount } from 'enzyme'; -import { EuiLink, EuiButton, EuiButtonEmpty, EuiPanel, EuiCard } from '@elastic/eui'; -import { mockKibanaValues, mockHistory } from '../../__mocks__'; +import { EuiLink, EuiButton, EuiButtonEmpty, EuiPanel, EuiCard } from '@elastic/eui'; import { EuiLinkTo, EuiButtonTo, EuiButtonEmptyTo, EuiPanelTo, EuiCardTo } from './eui_components'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx index 384eb79c993c1..b9fee9d16273b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues } from 'kea'; + import { EuiLink, EuiButton, @@ -20,8 +22,9 @@ import { } from '@elastic/eui'; import { EuiPanelProps } from '@elastic/eui/src/components/panel/panel'; -import { KibanaLogic } from '../kibana'; import { HttpLogic } from '../http'; +import { KibanaLogic } from '../kibana'; + import { letBrowserHandleEvent, createHref } from './'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx index c0bd1f3671f15..88c170b059d9c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx @@ -6,16 +6,17 @@ */ import React from 'react'; + import { shallow, mount } from 'enzyme'; +import { EuiFieldText, EuiModal, EuiSelect } from '@elastic/eui'; + import { NUMBER } from '../constants/field_types'; import { FIELD_NAME_CORRECTED_PREFIX } from './constants'; import { SchemaAddFieldModal } from './'; -import { EuiFieldText, EuiModal, EuiSelect } from '@elastic/eui'; - describe('SchemaAddFieldModal', () => { const addNewField = jest.fn(); const closeAddFieldModal = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx index b1fde05906d44..a82f9e9b6113b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx @@ -6,11 +6,13 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiAccordion, EuiTableRow } from '@elastic/eui'; import { EuiLinkTo } from '../react_router_helpers'; + import { SchemaErrorsAccordion } from './schema_errors_accordion'; describe('SchemaErrorsAccordion', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx index 62f66bc95a5eb..5e89dce24bd4a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiSelect } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx index 5d69a8ea84acf..0136f9745c322 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx @@ -5,11 +5,13 @@ * 2.0. */ +import { mountWithIntl } from '../../../__mocks__'; + import React from 'react'; + import { shallow } from 'enzyme'; -import { EuiSteps, EuiLink } from '@elastic/eui'; -import { mountWithIntl } from '../../../__mocks__'; +import { EuiSteps, EuiLink } from '@elastic/eui'; import { CloudSetupInstructions } from './instructions'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx index 24ba5bd4e5d0a..b355c88943a54 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx @@ -6,9 +6,10 @@ */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; + import { EuiPageContent, EuiSteps, EuiText, EuiLink, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { docLinks } from '../../doc_links'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.test.tsx index 74fb74ce8cf70..fd31ca720b82b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.test.tsx @@ -5,11 +5,13 @@ * 2.0. */ +import { mountWithIntl } from '../../__mocks__'; + import React from 'react'; + import { shallow } from 'enzyme'; -import { EuiSteps, EuiLink } from '@elastic/eui'; -import { mountWithIntl } from '../../__mocks__'; +import { EuiSteps, EuiLink } from '@elastic/eui'; import { SetupInstructions } from './instructions'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx index 83c244ea24ff1..5e39d1acdf189 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; + import { EuiPageContent, EuiSpacer, @@ -18,6 +17,8 @@ import { EuiAccordion, EuiLink, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; interface Props { productName: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx index 0b70bb70f8441..90ddddd7d20aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx @@ -8,11 +8,13 @@ import { setMockValues, rerender } from '../../__mocks__'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiIcon } from '@elastic/eui'; -import { SetupInstructions } from './instructions'; import { CloudSetupInstructions } from './cloud/instructions'; +import { SetupInstructions } from './instructions'; import { SetupGuideLayout } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx index a0e89bfd8e57d..2140b3392abae 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { useValues } from 'kea'; import { @@ -22,9 +23,9 @@ import { import { KibanaLogic } from '../kibana'; -import { SetupInstructions } from './instructions'; import { CloudSetupInstructions } from './cloud/instructions'; import { SETUP_GUIDE_TITLE } from './constants'; +import { SetupInstructions } from './instructions'; import './setup_guide.scss'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx index d2588ed8d4aca..a481f22095aa3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui'; import { TableHeader } from './table_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx index 60f4d404a917a..5fc8074d0a4d7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx @@ -9,6 +9,7 @@ import '../../__mocks__/shallow_useeffect.mock'; import { mockTelemetryActions } from '../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx index e0f54a9e421bf..1759b4075deca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx @@ -6,6 +6,7 @@ */ import React, { useEffect } from 'react'; + import { useActions } from 'kea'; import { TelemetryLogic, SendTelemetryHelper } from './telemetry_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts index 52aec2c384adb..e516daedc1ba6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { JSON_HEADER as headers } from '../../../../common/constants'; import { LogicMounter, mockHttpValues } from '../../__mocks__'; +import { JSON_HEADER as headers } from '../../../../common/constants'; + import { TelemetryLogic } from './telemetry_logic'; describe('Telemetry logic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx index 71ed60cbd1c93..f9bf55b4fe800 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { TruncatedContent } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index c249f5ee20588..ce92f62d3a017 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { mergeServerAndStaticData } from '../views/content_sources/sources_logic'; -import { staticSourceData } from '../views/content_sources/source_data'; import { groups } from './groups.mock'; +import { staticSourceData } from '../views/content_sources/source_data'; +import { mergeServerAndStaticData } from '../views/content_sources/sources_logic'; + export const contentSources = [ { id: '123', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts index f1e6ca237681f..8ba94e83d26cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { LogicMounter } from '../__mocks__'; -import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { AppLogic } from './app_logic'; describe('AppLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx index c2c645ebe439a..a7a788b48789a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx @@ -5,12 +5,14 @@ * 2.0. */ -import { externalUrl } from '../../../shared/enterprise_search_url'; - import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiButtonEmpty } from '@elastic/eui'; +import { externalUrl } from '../../../shared/enterprise_search_url'; + import { WorkplaceSearchHeaderActions } from './'; describe('WorkplaceSearchHeaderActions', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx index c1912deb8d40a..c79865d25ecd7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx @@ -6,10 +6,10 @@ */ import React from 'react'; + import { EuiButtonEmpty, EuiText } from '@elastic/eui'; import { externalUrl, getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; - import { NAV } from '../../constants'; export const WorkplaceSearchHeaderActions: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx index d2b2da1a48176..8f37f608f4e28 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx @@ -8,9 +8,11 @@ import '../../../__mocks__/enterprise_search_url.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { SideNav, SideNavLink } from '../../../shared/layout'; + import { WorkplaceSearchNav } from './'; describe('WorkplaceSearchNav', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index 2696e5acf1c12..c184247b253d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -12,9 +12,7 @@ import { EuiSpacer } from '@elastic/eui'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { SideNav, SideNavLink } from '../../../shared/layout'; - import { NAV } from '../../constants'; - import { SOURCES_PATH, SECURITY_PATH, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/api_key/api_key.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/api_key/api_key.test.tsx index 66e9ac9ed7a8b..32f21c158736f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/api_key/api_key.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/api_key/api_key.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCodeBlock, EuiFormLabel } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.test.tsx index cb3fc32432999..991c7a061b4bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiLoadingSpinner, EuiTextColor } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx index c21e8b8d3449f..21280926d7aae 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx @@ -6,12 +6,15 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiSpacer } from '@elastic/eui'; -import { ContentSection } from './'; import { ViewContentHeader } from '../view_content_header'; +import { ContentSection } from './'; + const props = { children:
, testSubj: 'contentSection', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx index b0ab18bbfde95..e606263ac6f1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { EuiSpacer } from '@elastic/eui'; import { SpacerSizeTypes } from '../../../types'; - import { ViewContentHeader } from '../view_content_header'; import './content_section.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx index 79c4abdf2e223..13e2a229b3a76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx @@ -6,6 +6,7 @@ */ import * as React from 'react'; + import { shallow } from 'enzyme'; import { EuiCopy, EuiButtonIcon, EuiFieldText } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx index 4e38894766d86..6deb37d850076 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiBadge } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx index d8266127a0f42..6a69178ad07da 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiLink, EuiText } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx index 5ef191d0d0fe8..0bced6a7fc4e0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx @@ -9,7 +9,9 @@ import '../../../../__mocks__/kea.mock'; import { mockTelemetryActions } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx index ded4278c35e14..3611bfb2a3f69 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx @@ -6,13 +6,14 @@ */ import React from 'react'; + import { useActions } from 'kea'; import { EuiButton, EuiButtonProps, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TelemetryLogic } from '../../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../../shared/enterprise_search_url'; +import { TelemetryLogic } from '../../../../shared/telemetry'; export const ProductButton: React.FC = () => { const { sendWorkplaceSearchTelemetry } = useActions(TelemetryLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx index e5cd2bb2e0461..9af91107d7304 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ApiKey } from '../api_key'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx index 8c6e2b0174eb0..236d475b8f687 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx @@ -16,7 +16,6 @@ import { CLIENT_ID_LABEL, CLIENT_SECRET_LABEL, } from '../../../constants'; - import { ApiKey } from '../api_key'; import { CredentialItem } from '../credential_item'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx index f8cf9d63915d6..3bea6f224dc2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiIcon } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx index cfac7148bf88a..9661471bb1dd7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx @@ -5,11 +5,13 @@ * 2.0. */ +import { contentSources } from '../../../__mocks__/content_sources.mock'; + import React from 'react'; + import { shallow } from 'enzyme'; import { EuiTableRow, EuiSwitch, EuiIcon } from '@elastic/eui'; -import { contentSources } from '../../../__mocks__/content_sources.mock'; import { SourceIcon } from '../source_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx index 11d71481751b0..6cfc68b45ee3c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx @@ -26,14 +26,13 @@ import { import { EuiLinkTo } from '../../../../shared/react_router_helpers'; import { SOURCE_STATUSES as statuses } from '../../../constants'; -import { ContentSourceDetails } from '../../../types'; import { ADD_SOURCE_PATH, SOURCE_DETAILS_PATH, getContentSourcePath, getSourcesPath, } from '../../../routes'; - +import { ContentSourceDetails } from '../../../types'; import { SourceIcon } from '../source_icon'; import './source_row.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx index efe529bcfb289..f54f7ccdf24bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx @@ -5,13 +5,15 @@ * 2.0. */ +import { contentSources } from '../../../__mocks__/content_sources.mock'; + import React from 'react'; + import { shallow } from 'enzyme'; import { EuiTable } from '@elastic/eui'; -import { TableHeader } from '../../../../shared/table_header/table_header'; -import { contentSources } from '../../../__mocks__/content_sources.mock'; +import { TableHeader } from '../../../../shared/table_header/table_header'; import { SourceRow } from '../source_row'; import { SourcesTable } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx index a0aba097d17f4..66e7e2e752a1e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx @@ -10,8 +10,8 @@ import React from 'react'; import { EuiTable, EuiTableBody } from '@elastic/eui'; import { TableHeader } from '../../../../shared/table_header/table_header'; -import { SourceRow, ISourceRow } from '../source_row'; import { ContentSourceDetails } from '../../../types'; +import { SourceRow, ISourceRow } from '../source_row'; interface SourcesTableProps extends ISourceRow { sources: ContentSourceDetails[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx index 343c9b68bc834..d22ddcce49dc4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiFlexGroup, EuiTablePagination } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx index d8046bd88cf4a..5ce83b641cf8f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx @@ -5,10 +5,11 @@ * 2.0. */ +import { users } from '../../../__mocks__/users.mock'; + import React from 'react'; -import { shallow } from 'enzyme'; -import { users } from '../../../__mocks__/users.mock'; +import { shallow } from 'enzyme'; import { UserIcon } from './user_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx index fe2bfd27db55a..f15c74ed1054b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx @@ -5,13 +5,14 @@ * 2.0. */ +import { users } from '../../../__mocks__/users.mock'; + import React from 'react'; + import { shallow } from 'enzyme'; import { EuiTableRow } from '@elastic/eui'; -import { users } from '../../../__mocks__/users.mock'; - import { UserRow } from './'; describe('SourcesTable', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx index 01a05a5d94c75..fda1a27e103c2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlexGroup } from '@elastic/eui'; import { ViewContentHeader } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx index ed2989de5ce3c..fa3a1d3ccb2e4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle, EuiSpacer } from '@elastic/eui'; - import { FlexGroupAlignItems } from '@elastic/eui/src/components/flex/flex_group'; interface ViewContentHeaderProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 73ee7662888bb..5678ad545d50d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -10,13 +10,15 @@ import { setMockValues, setMockActions, mockKibanaValues } from '../__mocks__'; import React from 'react'; import { Redirect } from 'react-router-dom'; + import { shallow } from 'enzyme'; import { Layout } from '../shared/layout'; + import { WorkplaceSearchHeaderActions } from './components/layout'; -import { SetupGuide } from './views/setup_guide'; import { ErrorState } from './views/error_state'; import { Overview } from './views/overview'; +import { SetupGuide } from './views/setup_guide'; import { WorkplaceSearch, WorkplaceSearchUnconfigured, WorkplaceSearchConfigured } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index a4c12f1d71d4e..d690dee4dc98c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -7,16 +7,18 @@ import React, { useEffect } from 'react'; import { Route, Redirect, Switch, useLocation } from 'react-router-dom'; + import { useActions, useValues } from 'kea'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; import { InitialAppData } from '../../../common/types'; -import { KibanaLogic } from '../shared/kibana'; import { HttpLogic } from '../shared/http'; -import { AppLogic } from './app_logic'; +import { KibanaLogic } from '../shared/kibana'; import { Layout } from '../shared/layout'; -import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout'; +import { NotFound } from '../shared/not_found'; +import { AppLogic } from './app_logic'; +import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout'; import { GROUPS_PATH, SETUP_GUIDE_PATH, @@ -25,19 +27,16 @@ import { ORG_SETTINGS_PATH, SECURITY_PATH, } from './routes'; - -import { SetupGuide } from './views/setup_guide'; +import { SourcesRouter } from './views/content_sources'; +import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; import { ErrorState } from './views/error_state'; -import { NotFound } from '../shared/not_found'; -import { Overview } from './views/overview'; import { GroupsRouter } from './views/groups'; +import { GroupSubNav } from './views/groups/components/group_sub_nav'; +import { Overview } from './views/overview'; import { Security } from './views/security'; -import { SourcesRouter } from './views/content_sources'; import { SettingsRouter } from './views/settings'; - -import { GroupSubNav } from './views/groups/components/group_sub_nav'; -import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; import { SettingsSubNav } from './views/settings/components/settings_sub_nav'; +import { SetupGuide } from './views/setup_guide'; export const WorkplaceSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx index d9c1dbeefad92..68bec94270a01 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiLink } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx index aa219a475406f..41f53523bca4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx @@ -7,10 +7,10 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { mockKibanaValues, setMockActions, setMockValues } from '../../../../../__mocks__'; - import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { Loading } from '../../../../../shared/loading'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index 0664c930775bc..b00f9807f0acd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -9,16 +9,16 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { AppLogic } from '../../../../app_logic'; import { KibanaLogic } from '../../../../../shared/kibana'; import { Loading } from '../../../../../shared/loading'; +import { AppLogic } from '../../../../app_logic'; import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; -import { staticSourceData } from '../../source_data'; -import { AddSourceLogic, AddSourceProps, AddSourceSteps } from './add_source_logic'; -import { SourceDataItem } from '../../../../types'; import { SOURCE_ADDED_PATH, getSourcesPath } from '../../../../routes'; +import { SourceDataItem } from '../../../../types'; +import { staticSourceData } from '../../source_data'; import { AddSourceHeader } from './add_source_header'; +import { AddSourceLogic, AddSourceProps, AddSourceSteps } from './add_source_logic'; import { ConfigCompleted } from './config_completed'; import { ConfigurationIntro } from './configuration_intro'; import { ConfigureCustom } from './configure_custom'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.test.tsx index 7167fcf3bc252..879f7993f3dc1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiText, EuiTextColor } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx index fe24eeb5c7cb5..6da348c6e2755 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx @@ -14,6 +14,7 @@ import { } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; @@ -21,16 +22,15 @@ import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; import { Loading } from '../../../../../../applications/shared/loading'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { AddSourceList } from './add_source_list'; +import { AvailableSourcesList } from './available_sources_list'; +import { ConfiguredSourcesList } from './configured_sources_list'; import { ADD_SOURCE_NEW_SOURCE_DESCRIPTION, ADD_SOURCE_ORG_SOURCE_DESCRIPTION, ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION, } from './constants'; -import { AddSourceList } from './add_source_list'; -import { AvailableSourcesList } from './available_sources_list'; -import { ConfiguredSourcesList } from './configured_sources_list'; - describe('AddSourceList', () => { const initializeSources = jest.fn(); const resetSourcesState = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index 3a0db0f44047d..d026782d12540 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -18,15 +18,18 @@ import { EuiPanel, EuiEmptyPrompt, } from '@elastic/eui'; -import noSharedSourcesIcon from '../../../../assets/share_circle.svg'; +import { Loading } from '../../../../../../applications/shared/loading'; import { AppLogic } from '../../../../app_logic'; +import noSharedSourcesIcon from '../../../../assets/share_circle.svg'; import { ContentSection } from '../../../../components/shared/content_section'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { Loading } from '../../../../../../applications/shared/loading'; import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; import { SourceDataItem } from '../../../../types'; +import { SourcesLogic } from '../../sources_logic'; +import { AvailableSourcesList } from './available_sources_list'; +import { ConfiguredSourcesList } from './configured_sources_list'; import { ADD_SOURCE_NEW_SOURCE_DESCRIPTION, ADD_SOURCE_ORG_SOURCE_DESCRIPTION, @@ -39,10 +42,6 @@ import { ADD_SOURCE_EMPTY_BODY, } from './constants'; -import { SourcesLogic } from '../../sources_logic'; -import { AvailableSourcesList } from './available_sources_list'; -import { ConfiguredSourcesList } from './configured_sources_list'; - export const AddSourceList: React.FC = () => { const { contentSources, dataLoading, availableSources, configuredSources } = useValues( SourcesLogic diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index a3fd35503ea0d..ed67eb9994bc8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -11,20 +11,18 @@ import { mockHttpValues, mockKibanaValues, } from '../../../../../__mocks__'; +import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; + +import { nextTick } from '@kbn/test/jest'; -import { AppLogic } from '../../../../app_logic'; jest.mock('../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { AppLogic } from '../../../../app_logic'; -import { SourcesLogic } from '../../sources_logic'; - -import { nextTick } from '@kbn/test/jest'; - -import { CustomSource } from '../../../../types'; import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; - -import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; +import { CustomSource } from '../../../../types'; +import { SourcesLogic } from '../../sources_logic'; import { AddSourceLogic, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index f10e81487567e..4e996aff6f5b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -5,33 +5,27 @@ * 2.0. */ -import { keys, pickBy } from 'lodash'; - -import { kea, MakeLogicType } from 'kea'; - import { Search } from 'history'; +import { kea, MakeLogicType } from 'kea'; +import { keys, pickBy } from 'lodash'; import { i18n } from '@kbn/i18n'; - import { HttpFetchQuery } from 'src/core/public'; -import { HttpLogic } from '../../../../../shared/http'; -import { KibanaLogic } from '../../../../../shared/kibana'; -import { parseQueryParams } from '../../../../../shared/query_params'; - import { flashAPIErrors, setSuccessMessage, clearFlashMessages, } from '../../../../../shared/flash_messages'; - -import { staticSourceData } from '../../source_data'; -import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; -import { CUSTOM_SERVICE_TYPE, WORKPLACE_SEARCH_URL_PREFIX } from '../../../../constants'; - +import { HttpLogic } from '../../../../../shared/http'; +import { KibanaLogic } from '../../../../../shared/kibana'; +import { parseQueryParams } from '../../../../../shared/query_params'; import { AppLogic } from '../../../../app_logic'; -import { SourcesLogic } from '../../sources_logic'; +import { CUSTOM_SERVICE_TYPE, WORKPLACE_SEARCH_URL_PREFIX } from '../../../../constants'; +import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; import { CustomSource } from '../../../../types'; +import { staticSourceData } from '../../source_data'; +import { SourcesLogic } from '../../sources_logic'; export interface AddSourceProps { sourceIndex: number; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx index 43f1486644c72..fcb55f24ddb03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx @@ -7,10 +7,10 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__'; - import { mergedAvailableSources } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCard, EuiToolTip, EuiTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx index 8060f765a91b0..fafc1ea54a6cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; +import { useValues } from 'kea'; import { EuiCard, @@ -18,15 +18,13 @@ import { EuiText, EuiToolTip, } from '@elastic/eui'; - -import { useValues } from 'kea'; +import { i18n } from '@kbn/i18n'; import { LicensingLogic } from '../../../../../shared/licensing'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; - import { SourceIcon } from '../../../../components/shared/source_icon'; -import { SourceDataItem } from '../../../../types'; import { ADD_CUSTOM_PATH, getSourcesPath } from '../../../../routes'; +import { SourceDataItem } from '../../../../types'; import { AVAILABLE_SOURCE_EMPTY_STATE, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx index cd40da7f6b376..163da5297e370 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ConfigCompleted } from './config_completed'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx index bd85b3c7c2dd5..1d4f1f2fca980 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx @@ -7,9 +7,6 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiButton, EuiFlexGroup, @@ -20,7 +17,10 @@ import { EuiText, EuiTextAlign, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLinkTo, EuiButtonTo } from '../../../../../shared/react_router_helpers'; import { getSourcesPath, ADD_SOURCE_PATH, @@ -28,8 +28,6 @@ import { PRIVATE_SOURCES_DOCS_URL, } from '../../../../routes'; -import { EuiLinkTo, EuiButtonTo } from '../../../../../shared/react_router_helpers'; - import { CONFIG_COMPLETED_PRIVATE_SOURCES_DOCS_LINK, CONFIG_COMPLETED_CONFIGURE_NEW_BUTTON, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.test.tsx index b56f36df5486e..914eca94ad6f3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiButtonEmpty } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx index 259e911d6d54f..043d28e9dba03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx @@ -7,9 +7,8 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; - import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DOCUMENTATION_LINK_TITLE } from '../../../../constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx index 2d26982cbc2f5..2ebc021925abf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiText, EuiTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx index ff1caafb91bdb..914eee74dfc4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx @@ -7,9 +7,6 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiBadge, EuiButton, @@ -20,6 +17,10 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import connectionIllustration from '../../../../assets/connection_illustration.svg'; import { CONFIG_INTRO_ALT_TEXT, @@ -31,8 +32,6 @@ import { CONFIG_INTRO_STEP2_TEXT, } from './constants'; -import connectionIllustration from '../../../../assets/connection_illustration.svg'; - interface ConfigurationIntroProps { header: React.ReactNode; name: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx index a3b572737bdeb..099989255bf47 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx @@ -9,6 +9,7 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiForm, EuiFieldText } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx index 2d0113f1d0e7d..36242f5523e77 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx @@ -9,8 +9,6 @@ import React, { ChangeEvent, FormEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiButton, EuiFieldText, @@ -20,8 +18,10 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { CUSTOM_SOURCE_DOCS_URL } from '../../../../routes'; + import { AddSourceLogic } from './add_source_logic'; import { CONFIG_CUSTOM_BUTTON } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx index a57ff390150ea..985488558c984 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx @@ -9,6 +9,7 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCheckboxGroup } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx index 3eae438eb960c..eb7b61ef658db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx @@ -6,10 +6,10 @@ */ import React, { useEffect, useState, FormEvent } from 'react'; +import { useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions, useValues } from 'kea'; -import { useLocation } from 'react-router-dom'; import { EuiButton, @@ -19,13 +19,12 @@ import { EuiFormRow, EuiSpacer, } from '@elastic/eui'; - import { EuiCheckboxGroupIdToSelectedMap } from '@elastic/eui/src/components/form/checkbox/checkbox_group'; -import { parseQueryParams } from '../../../../../../applications/shared/query_params'; import { Loading } from '../../../../../../applications/shared/loading'; -import { AddSourceLogic } from './add_source_logic'; +import { parseQueryParams } from '../../../../../../applications/shared/query_params'; +import { AddSourceLogic } from './add_source_logic'; import { CONFIG_OAUTH_LABEL, CONFIG_OAUTH_BUTTON } from './constants'; interface OauthQueryParams { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx index 3bb7d42748f25..2e2e04556cdb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx @@ -5,13 +5,14 @@ * 2.0. */ +import { mergedConfiguredSources } from '../../../../__mocks__/content_sources.mock'; + import React from 'react'; + import { shallow } from 'enzyme'; import { EuiPanel } from '@elastic/eui'; -import { mergedConfiguredSources } from '../../../../__mocks__/content_sources.mock'; - import { ConfiguredSourcesList } from './configured_sources_list'; describe('ConfiguredSourcesList', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx index cb5e96a4019a1..5f64913410d4c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx @@ -21,8 +21,8 @@ import { import { EuiButtonEmptyTo } from '../../../../../shared/react_router_helpers'; import { SourceIcon } from '../../../../components/shared/source_icon'; -import { SourceDataItem } from '../../../../types'; import { getSourcesPath } from '../../../../routes'; +import { SourceDataItem } from '../../../../types'; import { CONFIGURED_SOURCES_LIST_UNCONNECTED_TOOLTIP, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx index cff95136968db..b795b0af09944 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx @@ -9,12 +9,14 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiBadge, EuiCallOut, EuiSwitch } from '@elastic/eui'; import { FeatureIds } from '../../../../types'; import { staticSourceData } from '../../source_data'; + import { ConnectInstance } from './connect_instance'; describe('ConnectInstance', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index ea5d556350759..d85bd21c54e5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -8,8 +8,6 @@ import React, { useState, useEffect, FormEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, @@ -27,16 +25,16 @@ import { EuiBadge, EuiBadgeGroup, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { LicensingLogic } from '../../../../../../applications/shared/licensing'; - import { AppLogic } from '../../../../app_logic'; -import { AddSourceLogic } from './add_source_logic'; -import { FeatureIds, Configuration, Features } from '../../../../types'; import { DOCUMENT_PERMISSIONS_DOCS_URL } from '../../../../routes'; -import { SourceFeatures } from './source_features'; - +import { FeatureIds, Configuration, Features } from '../../../../types'; import { LEARN_MORE_LINK } from '../../constants'; + +import { AddSourceLogic } from './add_source_logic'; import { CONNECT_REMOTE, CONNECT_PRIVATE, @@ -47,6 +45,7 @@ import { CONNECT_NOT_SYNCED_TITLE, CONNECT_NOT_SYNCED_TEXT, } from './constants'; +import { SourceFeatures } from './source_features'; interface ConnectInstanceProps { header: React.ReactNode; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx index 94c2e734751ee..38b6925008181 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx @@ -9,6 +9,7 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { ReAuthenticate } from './re_authenticate'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx index 0cdf461f2d64c..15082f6de85bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx @@ -6,13 +6,14 @@ */ import React, { useEffect, useState, FormEvent } from 'react'; +import { useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions, useValues } from 'kea'; -import { useLocation } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { parseQueryParams } from '../../../../../../applications/shared/query_params'; import { AddSourceLogic } from './add_source_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.test.tsx index 4d1955d7928a8..c0f7f1139cb73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.test.tsx @@ -7,18 +7,18 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__'; +import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiSteps, EuiButton, EuiButtonEmpty } from '@elastic/eui'; -import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; +import { ApiKey } from '../../../../components/shared/api_key'; import { staticSourceData } from '../../source_data'; -import { ApiKey } from '../../../../components/shared/api_key'; import { ConfigDocsLinks } from './config_docs_links'; - import { SaveConfig } from './save_config'; describe('SaveConfig', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx index f6d5d0f4066ab..06ee2b6eb40f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx @@ -8,7 +8,6 @@ import React, { FormEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiButton, @@ -21,7 +20,10 @@ import { EuiSpacer, EuiSteps, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { LicensingLogic } from '../../../../../../applications/shared/licensing'; +import { ApiKey } from '../../../../components/shared/api_key'; import { PUBLIC_KEY_LABEL, CONSUMER_KEY_LABEL, @@ -31,16 +33,11 @@ import { CLIENT_SECRET_LABEL, REMOVE_BUTTON, } from '../../../../constants'; - -import { OAUTH_SAVE_CONFIG_BUTTON, OAUTH_BACK_BUTTON, OAUTH_STEP_2 } from './constants'; - -import { LicensingLogic } from '../../../../../../applications/shared/licensing'; - -import { ApiKey } from '../../../../components/shared/api_key'; -import { AddSourceLogic } from './add_source_logic'; import { Configuration } from '../../../../types'; +import { AddSourceLogic } from './add_source_logic'; import { ConfigDocsLinks } from './config_docs_links'; +import { OAUTH_SAVE_CONFIG_BUTTON, OAUTH_BACK_BUTTON, OAUTH_STEP_2 } from './constants'; interface SaveConfigProps { header: React.ReactNode; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx index 551bd7f1bb006..5ed777322cc08 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiLink, EuiPanel, EuiTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx index a61ad1aeb728a..b42bd674109fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx @@ -7,9 +7,6 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiFlexGroup, EuiFlexItem, @@ -22,12 +19,12 @@ import { EuiLink, EuiPanel, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { CredentialItem } from '../../../../components/shared/credential_item'; import { LicenseBadge } from '../../../../components/shared/license_badge'; - -import { CustomSource } from '../../../../types'; import { SOURCES_PATH, SOURCE_DISPLAY_SETTINGS_PATH, @@ -36,7 +33,7 @@ import { getContentSourcePath, getSourcesPath, } from '../../../../routes'; - +import { CustomSource } from '../../../../types'; import { ACCESS_TOKEN_LABEL, ID_LABEL, LEARN_CUSTOM_FEATURES_BUTTON } from '../../constants'; import { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.test.tsx index ccc6d05df5f9a..cd8ba37695ac6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.test.tsx @@ -11,10 +11,10 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; -import { SourceFeatures } from './source_features'; - import { staticSourceData } from '../../source_data'; +import { SourceFeatures } from './source_features'; + describe('SourceFeatures', () => { const { features, objTypes } = staticSourceData[0]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx index 186e8fcdc3790..f304a1a36d9dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { useValues } from 'kea'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, @@ -19,13 +18,13 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { LicensingLogic } from '../../../../../../applications/shared/licensing'; - import { AppLogic } from '../../../../app_logic'; import { LicenseBadge } from '../../../../components/shared/license_badge'; -import { Features, FeatureIds } from '../../../../types'; import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../../routes'; +import { Features, FeatureIds } from '../../../../types'; import { SOURCE_FEATURES_SEARCHABLE, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx index 6567f74bd8790..fcce69d70ad50 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { CustomSourceIcon } from './custom_source_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx index 72744690baf30..feebc7f8d445e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx @@ -8,24 +8,21 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { mockKibanaValues } from '../../../../../__mocks__'; - import { setMockValues, setMockActions } from '../../../../../__mocks__'; import { unmountHandler } from '../../../../../__mocks__/shallow_useeffect.mock'; - -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { EuiButton, EuiTabbedContent } from '@elastic/eui'; +import { shallow } from 'enzyme'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { EuiButton, EuiTabbedContent } from '@elastic/eui'; import { Loading } from '../../../../../shared/loading'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { FieldEditorModal } from './field_editor_modal'; - import { DisplaySettings } from './display_settings'; +import { FieldEditorModal } from './field_editor_modal'; describe('DisplaySettings', () => { const { navigateToUrl } = mockKibanaValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index 62beb4e40793b..29266cdefe584 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -19,21 +19,18 @@ import { EuiTabbedContentTab, } from '@elastic/eui'; +import { clearFlashMessages } from '../../../../../shared/flash_messages'; +import { KibanaLogic } from '../../../../../shared/kibana'; +import { Loading } from '../../../../../shared/loading'; +import { AppLogic } from '../../../../app_logic'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { SAVE_BUTTON } from '../../../../constants'; import { DISPLAY_SETTINGS_RESULT_DETAIL_PATH, DISPLAY_SETTINGS_SEARCH_RESULT_PATH, getContentSourcePath, } from '../../../../routes'; -import { clearFlashMessages } from '../../../../../shared/flash_messages'; - -import { KibanaLogic } from '../../../../../shared/kibana'; -import { AppLogic } from '../../../../app_logic'; - -import { Loading } from '../../../../../shared/loading'; -import { ViewContentHeader } from '../../../../components/shared/view_content_header'; - -import { SAVE_BUTTON } from '../../../../constants'; import { UNSAVED_MESSAGE, DISPLAY_SETTINGS_TITLE, @@ -43,9 +40,7 @@ import { SEARCH_RESULTS_LABEL, RESULT_DETAIL_LABEL, } from './constants'; - import { DisplaySettingsLogic } from './display_settings_logic'; - import { FieldEditorModal } from './field_editor_modal'; import { ResultDetail } from './result_detail'; import { SearchResults } from './search_results'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts index c51f3e97bf155..73df0298ecd19 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts @@ -5,25 +5,22 @@ * 2.0. */ -import { LogicMounter } from '../../../../../__mocks__/kea.mock'; +import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; -import { mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; +import { nextTick } from '@kbn/test/jest'; const contentSource = { id: 'source123' }; jest.mock('../../source_logic', () => ({ SourceLogic: { values: { contentSource } }, })); -import { AppLogic } from '../../../../app_logic'; jest.mock('../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { AppLogic } from '../../../../app_logic'; -import { nextTick } from '@kbn/test/jest'; - -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import { LEAVE_UNASSIGNED_FIELD } from './constants'; - import { DisplaySettingsLogic, defaultSearchResultConfig } from './display_settings_logic'; describe('DisplaySettingsLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index 7c5946d08292c..62d959083af59 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -5,24 +5,23 @@ * 2.0. */ -import { cloneDeep, isEqual, differenceBy } from 'lodash'; import { DropResult } from 'react-beautiful-dnd'; import { kea, MakeLogicType } from 'kea'; - -import { HttpLogic } from '../../../../../shared/http'; +import { cloneDeep, isEqual, differenceBy } from 'lodash'; import { setSuccessMessage, clearFlashMessages, flashAPIErrors, } from '../../../../../shared/flash_messages'; - +import { HttpLogic } from '../../../../../shared/http'; import { AppLogic } from '../../../../app_logic'; +import { DetailField, SearchResultConfig, OptionValue, Result } from '../../../../types'; import { SourceLogic } from '../../source_logic'; -import { DetailField, SearchResultConfig, OptionValue, Result } from '../../../../types'; import { LEAVE_UNASSIGNED_FIELD, SUCCESS_MESSAGE } from './constants'; + export interface DisplaySettingsResponseProps { sourceName: string; searchResultConfig: SearchResultConfig; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx index f216cbf286b94..f04afe60aa49d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx @@ -10,12 +10,11 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - import { Route, Switch } from 'react-router-dom'; -import { DisplaySettings } from './display_settings'; +import { shallow } from 'enzyme'; +import { DisplaySettings } from './display_settings'; import { DisplaySettingsRouter } from './display_settings_router'; describe('DisplaySettingsRouter', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx index fa9817494ee09..bd753631ed48c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx @@ -6,12 +6,11 @@ */ import React from 'react'; +import { Route, Switch } from 'react-router-dom'; import { useValues } from 'kea'; -import { Route, Switch } from 'react-router-dom'; import { AppLogic } from '../../../../app_logic'; - import { DISPLAY_SETTINGS_RESULT_DETAIL_PATH, DISPLAY_SETTINGS_SEARCH_RESULT_PATH, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx index 381e4fe4c0b25..15e1fe0ed417c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx @@ -8,11 +8,11 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__'; -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; import { ExampleResultDetailCard } from './example_result_detail_card'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx index 46c06f28af6d6..93a7d660215f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx @@ -14,9 +14,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elasti import { URL_LABEL } from '../../../../constants'; -import { DisplaySettingsLogic } from './display_settings_logic'; - import { CustomSourceIcon } from './custom_source_icon'; +import { DisplaySettingsLogic } from './display_settings_logic'; import { TitleField } from './title_field'; export const ExampleResultDetailCard: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx index d08195f3e83bc..6f90c1045ae31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx @@ -8,14 +8,13 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__'; -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; import { CustomSourceIcon } from './custom_source_icon'; - import { ExampleSearchResultGroup } from './example_search_result_group'; describe('ExampleSearchResultGroup', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx index 5d5f73467f82c..df89eed38ae92 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx @@ -7,15 +7,15 @@ import React from 'react'; -import { isColorDark, hexToRgb } from '@elastic/eui'; import classNames from 'classnames'; import { useValues } from 'kea'; -import { DESCRIPTION_LABEL } from '../../../../constants'; +import { isColorDark, hexToRgb } from '@elastic/eui'; -import { DisplaySettingsLogic } from './display_settings_logic'; +import { DESCRIPTION_LABEL } from '../../../../constants'; import { CustomSourceIcon } from './custom_source_icon'; +import { DisplaySettingsLogic } from './display_settings_logic'; import { SubtitleField } from './subtitle_field'; import { TitleField } from './title_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx index 6241bcf05fbff..49845e79d86aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx @@ -8,14 +8,13 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__'; -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; import { CustomSourceIcon } from './custom_source_icon'; - import { ExampleStandoutResult } from './example_standout_result'; describe('ExampleStandoutResult', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx index 3c139001d3ea2..48c3149e622bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx @@ -14,9 +14,8 @@ import { isColorDark, hexToRgb } from '@elastic/eui'; import { DESCRIPTION_LABEL } from '../../../../constants'; -import { DisplaySettingsLogic } from './display_settings_logic'; - import { CustomSourceIcon } from './custom_source_icon'; +import { DisplaySettingsLogic } from './display_settings_logic'; import { SubtitleField } from './subtitle_field'; import { TitleField } from './title_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx index 82687165d0535..fe7bced843841 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx @@ -8,13 +8,13 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../../__mocks__'; -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { EuiModal, EuiSelect, EuiFieldText } from '@elastic/eui'; +import { shallow } from 'enzyme'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { EuiModal, EuiSelect, EuiFieldText } from '@elastic/eui'; import { FieldEditorModal } from './field_editor_modal'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx index 217a8142af5d5..768573ce80fee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx @@ -6,9 +6,8 @@ */ import '../../../../../__mocks__/shallow_useeffect.mock'; - import { setMockValues, setMockActions } from '../../../../../__mocks__'; -import { shallow, mount } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; /** * Mocking necessary due to console warnings from react d-n-d, which EUI uses @@ -40,12 +39,11 @@ jest.mock('react-beautiful-dnd', () => ({ import React from 'react'; -import { EuiTextColor } from '@elastic/eui'; +import { shallow, mount } from 'enzyme'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { EuiTextColor } from '@elastic/eui'; import { ExampleResultDetailCard } from './example_result_detail_card'; - import { ResultDetail } from './result_detail'; describe('ResultDetail', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx index 8382ddc9e82b3..6832f075476e7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx @@ -28,10 +28,9 @@ import { } from '@elastic/eui'; import { ADD_FIELD_LABEL, EDIT_FIELD_LABEL, REMOVE_FIELD_LABEL } from '../../../../constants'; -import { VISIBLE_FIELDS_TITLE, EMPTY_FIELDS_DESCRIPTION, PREVIEW_TITLE } from './constants'; +import { VISIBLE_FIELDS_TITLE, EMPTY_FIELDS_DESCRIPTION, PREVIEW_TITLE } from './constants'; import { DisplaySettingsLogic } from './display_settings_logic'; - import { ExampleResultDetailCard } from './example_result_detail_card'; export const ResultDetail: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx index 26116a7e736bc..28de0006f162f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx @@ -8,16 +8,15 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../../__mocks__'; -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; +import { LEAVE_UNASSIGNED_FIELD } from './constants'; import { ExampleSearchResultGroup } from './example_search_result_group'; import { ExampleStandoutResult } from './example_standout_result'; - -import { LEAVE_UNASSIGNED_FIELD } from './constants'; import { SearchResults } from './search_results'; describe('SearchResults', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx index b2ba2b13e5ec3..859fb2d5d2a20 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx @@ -21,9 +21,8 @@ import { EuiTitle, } from '@elastic/eui'; -import { DisplaySettingsLogic } from './display_settings_logic'; - import { DESCRIPTION_LABEL } from '../../../../constants'; + import { LEAVE_UNASSIGNED_FIELD, SEARCH_RESULTS_TITLE, @@ -34,7 +33,7 @@ import { STANDARD_RESULTS_TITLE, STANDARD_RESULTS_DESCRIPTION, } from './constants'; - +import { DisplaySettingsLogic } from './display_settings_logic'; import { ExampleSearchResultGroup } from './example_search_result_group'; import { ExampleStandoutResult } from './example_standout_result'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx index 812af1b1fd26b..76c28ae3d4060 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SubtitleField } from './subtitle_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx index f2a82f058c0de..2ed4aa0b0fad1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { TitleField } from './title_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx index 0e91d2a3a4a28..a30f1bfbd596a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx @@ -8,14 +8,14 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../__mocks__'; +import { fullContentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiPanel, EuiTable } from '@elastic/eui'; -import { fullContentSources } from '../../../__mocks__/content_sources.mock'; - import { Loading } from '../../../../shared/loading'; import { ComponentLoader } from '../../../components/shared/component_loader'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index a67adfdd3802a..34d7edd99c376 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { useValues } from 'kea'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiEmptyPrompt, EuiFlexGroup, @@ -30,7 +28,21 @@ import { EuiTextColor, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Loading } from '../../../../shared/loading'; +import { EuiPanelTo } from '../../../../shared/react_router_helpers'; +import { AppLogic } from '../../../app_logic'; +import aclImage from '../../../assets/supports_acl.svg'; +import { ComponentLoader } from '../../../components/shared/component_loader'; +import { CredentialItem } from '../../../components/shared/credential_item'; +import { LicenseBadge } from '../../../components/shared/license_badge'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { + RECENT_ACTIVITY_TITLE, + CREDENTIALS_TITLE, + DOCUMENTATION_LINK_TITLE, +} from '../../../constants'; import { CUSTOM_SOURCE_DOCS_URL, DOCUMENT_PERMISSIONS_DOCS_URL, @@ -38,12 +50,6 @@ import { EXTERNAL_IDENTITIES_DOCS_URL, getGroupPath, } from '../../../routes'; - -import { - RECENT_ACTIVITY_TITLE, - CREDENTIALS_TITLE, - DOCUMENTATION_LINK_TITLE, -} from '../../../constants'; import { SOURCES_NO_CONTENT_TITLE, CONTENT_SUMMARY_TITLE, @@ -70,17 +76,6 @@ import { DOC_PERMISSIONS_DESCRIPTION, CUSTOM_CALLOUT_TITLE, } from '../constants'; - -import { AppLogic } from '../../../app_logic'; - -import { ComponentLoader } from '../../../components/shared/component_loader'; -import { CredentialItem } from '../../../components/shared/credential_item'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { LicenseBadge } from '../../../components/shared/license_badge'; -import { Loading } from '../../../../shared/loading'; -import { EuiPanelTo } from '../../../../shared/react_router_helpers'; - -import aclImage from '../../../assets/supports_acl.svg'; import { SourceLogic } from '../source_logic'; export const Overview: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx index b30c5d78fd42f..ccf3275ffd96f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx @@ -8,21 +8,20 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../../__mocks__'; +import { mostRecentIndexJob } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; -import { mostRecentIndexJob } from '../../../../__mocks__/content_sources.mock'; - import { IndexingStatus } from '../../../../../shared/indexing_status'; import { Loading } from '../../../../../shared/loading'; import { SchemaAddFieldModal } from '../../../../../shared/schema/schema_add_field_modal'; -import { SchemaFieldsTable } from './schema_fields_table'; - import { Schema } from './schema'; +import { SchemaFieldsTable } from './schema_fields_table'; describe('Schema', () => { const initializeSchema = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx index fe48e1c14ff41..77d1002a9ad26 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx @@ -20,17 +20,12 @@ import { EuiPanel, } from '@elastic/eui'; -import { getReindexJobRoute } from '../../../../routes'; -import { AppLogic } from '../../../../app_logic'; - +import { IndexingStatus } from '../../../../../shared/indexing_status'; import { Loading } from '../../../../../shared/loading'; -import { ViewContentHeader } from '../../../../components/shared/view_content_header'; - import { SchemaAddFieldModal } from '../../../../../shared/schema/schema_add_field_modal'; -import { IndexingStatus } from '../../../../../shared/indexing_status'; - -import { SchemaFieldsTable } from './schema_fields_table'; -import { SchemaLogic } from './schema_logic'; +import { AppLogic } from '../../../../app_logic'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { getReindexJobRoute } from '../../../../routes'; import { SCHEMA_ADD_FIELD_BUTTON, @@ -42,6 +37,8 @@ import { SCHEMA_EMPTY_SCHEMA_TITLE, SCHEMA_EMPTY_SCHEMA_DESCRIPTION, } from './constants'; +import { SchemaFieldsTable } from './schema_fields_table'; +import { SchemaLogic } from './schema_logic'; export const Schema: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx index 421aa04692bd7..e9276b8ec3878 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx @@ -10,9 +10,10 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; import { useParams } from 'react-router-dom'; +import { shallow } from 'enzyme'; + import { SchemaErrorsAccordion } from '../../../../../shared/schema/schema_errors_accordion'; import { SchemaChangeErrors } from './schema_change_errors'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx index a212052e1beba..29cb2b7589220 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx @@ -14,8 +14,9 @@ import { EuiSpacer } from '@elastic/eui'; import { SchemaErrorsAccordion } from '../../../../../shared/schema/schema_errors_accordion'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { SchemaLogic } from './schema_logic'; + import { SCHEMA_ERRORS_HEADING } from './constants'; +import { SchemaLogic } from './schema_logic'; export const SchemaChangeErrors: React.FC = () => { const { activeReindexJobId, sourceId } = useParams() as { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx index a9d6494dcee00..bc0363d55da69 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx @@ -10,6 +10,7 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { SchemaExistingField } from '../../../../../shared/schema/schema_existing_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx index a683d9384f636..3f56a2cfc745b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiFlexGroup, EuiFlexItem, @@ -21,13 +19,15 @@ import { EuiTableRow, EuiTableRowCell, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { SchemaExistingField } from '../../../../../shared/schema/schema_existing_field'; -import { SchemaLogic } from './schema_logic'; + import { SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER, SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER, } from './constants'; +import { SchemaLogic } from './schema_logic'; export const SchemaFieldsTable: React.FC = () => { const { updateExistingFieldType } = useActions(SchemaLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts index 5957822eb8d49..af650d95efaf6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts @@ -6,6 +6,7 @@ */ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; +import { mostRecentIndexJob } from '../../../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test/jest'; @@ -14,7 +15,6 @@ jest.mock('../../source_logic', () => ({ SourceLogic: { values: { contentSource } }, })); -import { AppLogic } from '../../../../app_logic'; jest.mock('../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); @@ -22,16 +22,15 @@ jest.mock('../../../../app_logic', () => ({ const spyScrollTo = jest.fn(); Object.defineProperty(global.window, 'scrollTo', { value: spyScrollTo }); -import { mostRecentIndexJob } from '../../../../__mocks__/content_sources.mock'; import { TEXT } from '../../../../../shared/constants/field_types'; import { ADD, UPDATE } from '../../../../../shared/constants/operations'; +import { AppLogic } from '../../../../app_logic'; import { SCHEMA_FIELD_ERRORS_ERROR_MESSAGE, SCHEMA_FIELD_ADDED_MESSAGE, SCHEMA_UPDATED_MESSAGE, } from './constants'; - import { SchemaLogic, dataTypeOptions } from './schema_logic'; describe('SchemaLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts index 09b608af43536..9906efe707d85 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts @@ -5,23 +5,20 @@ * 2.0. */ -import { cloneDeep, isEqual } from 'lodash'; import { kea, MakeLogicType } from 'kea'; - -import { HttpLogic } from '../../../../../shared/http'; +import { cloneDeep, isEqual } from 'lodash'; import { TEXT } from '../../../../../shared/constants/field_types'; import { ADD, UPDATE } from '../../../../../shared/constants/operations'; -import { IndexJob, TOperation, Schema, SchemaTypes } from '../../../../../shared/types'; -import { OptionValue } from '../../../../types'; - import { flashAPIErrors, setSuccessMessage, clearFlashMessages, } from '../../../../../shared/flash_messages'; - +import { HttpLogic } from '../../../../../shared/http'; +import { IndexJob, TOperation, Schema, SchemaTypes } from '../../../../../shared/types'; import { AppLogic } from '../../../../app_logic'; +import { OptionValue } from '../../../../types'; import { SourceLogic } from '../../source_logic'; import { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.test.tsx index d3256a86baebc..ddf89159b2675 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.test.tsx @@ -10,10 +10,10 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions } from '../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - import { useLocation } from 'react-router-dom'; +import { shallow } from 'enzyme'; + import { Loading } from '../../../../shared/loading'; import { SourceAdded } from './source_added'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx index 5901c06b3f66c..5f1d2ed0c81c3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx @@ -6,10 +6,10 @@ */ import React, { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions } from 'kea'; -import { useLocation } from 'react-router-dom'; import { Loading } from '../../../../shared/loading'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx index 6a773b81909a3..e904efb73afc8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx @@ -8,8 +8,11 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { fullContentSources, contentItems } from '../../../__mocks__/content_sources.mock'; +import { meta } from '../../../__mocks__/meta.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { @@ -21,12 +24,9 @@ import { EuiLink, } from '@elastic/eui'; -import { meta } from '../../../__mocks__/meta.mock'; -import { fullContentSources, contentItems } from '../../../__mocks__/content_sources.mock'; - +import { Loading } from '../../../../../applications/shared/loading'; import { DEFAULT_META } from '../../../../shared/constants'; import { ComponentLoader } from '../../../components/shared/component_loader'; -import { Loading } from '../../../../../applications/shared/loading'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { SourceContent } from './source_content'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx index 61676279ada03..bf18f88e47537 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -11,9 +11,6 @@ import { useActions, useValues } from 'kea'; import { startCase } from 'lodash'; import moment from 'moment'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiButton, EuiButtonEmpty, @@ -31,20 +28,17 @@ import { EuiTableRowCell, EuiLink, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; -import { SourceContentItem } from '../../../types'; - +import { Loading } from '../../../../../applications/shared/loading'; import { TruncatedContent } from '../../../../shared/truncate'; - -const MAX_LENGTH = 28; - import { ComponentLoader } from '../../../components/shared/component_loader'; -import { Loading } from '../../../../../applications/shared/loading'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; - import { CUSTOM_SERVICE_TYPE } from '../../../constants'; +import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; +import { SourceContentItem } from '../../../types'; import { NO_CONTENT_MESSAGE, CUSTOM_DOCUMENTATION_LINK, @@ -55,9 +49,10 @@ import { SOURCE_CONTENT_TITLE, CONTENT_LOADING_TEXT, } from '../constants'; - import { SourceLogic } from '../source_logic'; +const MAX_LENGTH = 28; + export const SourceContent: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx index 7a8a932f391e1..7c4c02cdc9819 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiBadge, EuiHealth, EuiText, EuiTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx index 8334c34d6c615..765836191ff00 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -18,7 +18,6 @@ import { } from '@elastic/eui'; import { SourceIcon } from '../../../components/shared/source_icon'; - import { REMOTE_SOURCE_LABEL, CREATED_LABEL, STATUS_LABEL, READY_TEXT } from '../constants'; interface SourceInfoCardProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx index d73da79375ffe..f13189afe8252 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx @@ -8,14 +8,14 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { fullContentSources, sourceConfigData } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiConfirmModal } from '@elastic/eui'; -import { fullContentSources, sourceConfigData } from '../../../__mocks__/content_sources.mock'; - import { SourceConfigFields } from '../../../components/shared/source_config_fields'; import { SourceSettings } from './source_settings'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index 2fa00c7f029f1..75a1779a1fda8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -6,12 +6,10 @@ */ import React, { useEffect, useState, ChangeEvent, FormEvent } from 'react'; +import { Link } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import { isEmpty } from 'lodash'; -import { Link } from 'react-router-dom'; - -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, @@ -23,7 +21,12 @@ import { EuiFlexItem, EuiFormRow, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AppLogic } from '../../../app_logic'; +import { ContentSection } from '../../../components/shared/content_section'; +import { SourceConfigFields } from '../../../components/shared/source_config_fields'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { CANCEL_BUTTON, OK_BUTTON, @@ -31,6 +34,8 @@ import { SAVE_CHANGES_BUTTON, REMOVE_BUTTON, } from '../../../constants'; +import { SourceDataItem } from '../../../types'; +import { AddSourceLogic } from '../components/add_source/add_source_logic'; import { SOURCE_SETTINGS_TITLE, SOURCE_SETTINGS_DESCRIPTION, @@ -41,16 +46,7 @@ import { SOURCE_REMOVE_TITLE, SOURCE_REMOVE_DESCRIPTION, } from '../constants'; - -import { ContentSection } from '../../../components/shared/content_section'; -import { SourceConfigFields } from '../../../components/shared/source_config_fields'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; - -import { SourceDataItem } from '../../../types'; -import { AppLogic } from '../../../app_logic'; -import { AddSourceLogic } from '../components/add_source/add_source_logic'; import { staticSourceData } from '../source_data'; - import { SourceLogic } from '../source_logic'; export const SourceSettings: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx index fe5545668e4ce..59f3bfb0a5611 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx @@ -8,12 +8,13 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; +import { SideNavLink } from '../../../../shared/layout'; import { CUSTOM_SERVICE_TYPE } from '../../../constants'; -import { SourceSubNav } from './source_sub_nav'; -import { SideNavLink } from '../../../../shared/layout'; +import { SourceSubNav } from './source_sub_nav'; describe('SourceSubNav', () => { it('renders empty when no group id present', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx index 739b9ec138f29..99cebd5ded585 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx @@ -6,15 +6,12 @@ */ import React from 'react'; + import { useValues } from 'kea'; +import { SideNavLink } from '../../../../shared/layout'; import { AppLogic } from '../../../app_logic'; import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants'; - -import { SourceLogic } from '../source_logic'; - -import { SideNavLink } from '../../../../shared/layout'; - import { getContentSourcePath, SOURCE_DETAILS_PATH, @@ -23,6 +20,7 @@ import { SOURCE_DISPLAY_SETTINGS_PATH, SOURCE_SETTINGS_PATH, } from '../../../routes'; +import { SourceLogic } from '../source_logic'; export const SourceSubNav: React.FC = () => { const { isOrganization } = useValues(AppLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx index 68addbacc5a23..b986658f19fb3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx @@ -8,18 +8,16 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; - -import { shallow } from 'enzyme'; +import { contentSources } from '../../__mocks__/content_sources.mock'; import React from 'react'; import { Redirect } from 'react-router-dom'; -import { contentSources } from '../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; import { Loading } from '../../../shared/loading'; import { SourcesTable } from '../../components/shared/sources_table'; import { ViewContentHeader } from '../../components/shared/view_content_header'; - import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { OrganizationSources } from './organization_sources'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx index 24c1f130da50d..4559003b4597f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx @@ -6,11 +6,16 @@ */ import React, { useEffect } from 'react'; +import { Link, Redirect } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { Link, Redirect } from 'react-router-dom'; import { EuiButton } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; +import { ContentSection } from '../../components/shared/content_section'; +import { SourcesTable } from '../../components/shared/sources_table'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { @@ -18,14 +23,7 @@ import { ORG_SOURCES_HEADER_TITLE, ORG_SOURCES_HEADER_DESCRIPTION, } from './constants'; - -import { Loading } from '../../../shared/loading'; -import { ContentSection } from '../../components/shared/content_section'; -import { SourcesTable } from '../../components/shared/sources_table'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; - import { SourcesLogic } from './sources_logic'; - import { SourcesView } from './sources_view'; export const OrganizationSources: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx index d68b451ffa6f5..61be7b6281e9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx @@ -13,10 +13,14 @@ import { EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { LicensingLogic } from '../../../../applications/shared/licensing'; - -import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; - +import { Loading } from '../../../shared/loading'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { AppLogic } from '../../app_logic'; import noSharedSourcesIcon from '../../assets/share_circle.svg'; +import { ContentSection } from '../../components/shared/content_section'; +import { SourcesTable } from '../../components/shared/sources_table'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { AND, @@ -34,16 +38,8 @@ import { LICENSE_CALLOUT_TITLE, LICENSE_CALLOUT_DESCRIPTION, } from './constants'; - -import { Loading } from '../../../shared/loading'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; -import { ContentSection } from '../../components/shared/content_section'; -import { SourcesTable } from '../../components/shared/sources_table'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; - -import { AppLogic } from '../../app_logic'; -import { SourcesView } from './sources_view'; import { SourcesLogic } from './sources_logic'; +import { SourcesView } from './sources_view'; // TODO: Remove this after links in Kibana sidenav interface SidebarLink { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index 5b34603bca68f..cdad8e07a88be 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; +import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants'; import { ADD_BOX_PATH, ADD_CONFLUENCE_PATH, @@ -62,11 +63,8 @@ import { ZENDESK_DOCS_URL, CUSTOM_SOURCE_DOCS_URL, } from '../../routes'; - import { FeatureIds, SourceDataItem } from '../../types'; -import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants'; - const connectStepDescription = { attachments: i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.connectStepDescription.attachments', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index 15df7ddc99395..d20d0576d11ce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -12,16 +12,16 @@ import { mockKibanaValues, expectedAsyncError, } from '../../../__mocks__'; +import { fullContentSources, contentItems } from '../../__mocks__/content_sources.mock'; +import { meta } from '../../__mocks__/meta.mock'; + +import { DEFAULT_META } from '../../../shared/constants'; -import { AppLogic } from '../../app_logic'; jest.mock('../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { AppLogic } from '../../app_logic'; -import { fullContentSources, contentItems } from '../../__mocks__/content_sources.mock'; -import { meta } from '../../__mocks__/meta.mock'; - -import { DEFAULT_META } from '../../../shared/constants'; import { NOT_FOUND_PATH } from '../../routes'; import { SourceLogic } from './source_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index c1f5d6033543f..72700ce42c75d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -9,17 +9,15 @@ import { kea, MakeLogicType } from 'kea'; import { i18n } from '@kbn/i18n'; -import { HttpLogic } from '../../../shared/http'; -import { KibanaLogic } from '../../../shared/kibana'; - +import { DEFAULT_META } from '../../../shared/constants'; import { flashAPIErrors, setSuccessMessage, setQueuedSuccessMessage, clearFlashMessages, } from '../../../shared/flash_messages'; - -import { DEFAULT_META } from '../../../shared/constants'; +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import { AppLogic } from '../../app_logic'; import { NOT_FOUND_PATH, SOURCES_PATH, getSourcesPath } from '../../routes'; import { ContentSourceFullData, Meta, DocumentSummaryItem, SourceContentItem } from '../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index cf3c075d0c1e9..004f7e5e45bfa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -8,20 +8,17 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; +import { contentSources } from '../../__mocks__/content_sources.mock'; import React from 'react'; -import { shallow } from 'enzyme'; import { useParams } from 'react-router-dom'; - import { Route, Switch } from 'react-router-dom'; -import { contentSources } from '../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; - -import { NAV } from '../../constants'; - import { Loading } from '../../../shared/loading'; +import { NAV } from '../../constants'; import { DisplaySettingsRouter } from './components/display_settings'; import { Overview } from './components/overview'; @@ -29,7 +26,6 @@ import { Schema } from './components/schema'; import { SchemaChangeErrors } from './components/schema/schema_change_errors'; import { SourceContent } from './components/source_content'; import { SourceSettings } from './components/source_settings'; - import { SourceRouter } from './source_router'; describe('SourceRouter', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index ac450441f8783..ef9788efbdaf2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -6,24 +6,19 @@ */ import React, { useEffect } from 'react'; +import { Route, Switch, useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import moment from 'moment'; -import { Route, Switch, useParams } from 'react-router-dom'; import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; - +import { AppLogic } from '../../app_logic'; import { NAV } from '../../constants'; - -import { - SOURCE_DISABLED_CALLOUT_TITLE, - SOURCE_DISABLED_CALLOUT_DESCRIPTION, - SOURCE_DISABLED_CALLOUT_BUTTON, -} from './constants'; - +import { CUSTOM_SERVICE_TYPE } from '../../constants'; import { ENT_SEARCH_LICENSE_MANAGEMENT, REINDEX_JOB_PATH, @@ -36,13 +31,6 @@ import { getSourcesPath, } from '../../routes'; -import { AppLogic } from '../../app_logic'; - -import { Loading } from '../../../shared/loading'; - -import { CUSTOM_SERVICE_TYPE } from '../../constants'; -import { SourceLogic } from './source_logic'; - import { DisplaySettingsRouter } from './components/display_settings'; import { Overview } from './components/overview'; import { Schema } from './components/schema'; @@ -50,6 +38,12 @@ import { SchemaChangeErrors } from './components/schema/schema_change_errors'; import { SourceContent } from './components/source_content'; import { SourceInfoCard } from './components/source_info_card'; import { SourceSettings } from './components/source_settings'; +import { + SOURCE_DISABLED_CALLOUT_TITLE, + SOURCE_DISABLED_CALLOUT_DESCRIPTION, + SOURCE_DISABLED_CALLOUT_BUTTON, +} from './constants'; +import { SourceLogic } from './source_logic'; export const SourceRouter: React.FC = () => { const { sourceId } = useParams() as { sourceId: string }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts index b7db569eb704c..13844f51b2319 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts @@ -11,13 +11,12 @@ import { mockHttpValues, expectedAsyncError, } from '../../../__mocks__'; +import { configuredSources, contentSources } from '../../__mocks__/content_sources.mock'; -import { AppLogic } from '../../app_logic'; jest.mock('../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); - -import { configuredSources, contentSources } from '../../__mocks__/content_sources.mock'; +import { AppLogic } from '../../app_logic'; import { SourcesLogic, fetchSourceStatuses, POLLING_INTERVAL } from './sources_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts index 5108ed45501f7..9de2b447619a6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -5,22 +5,18 @@ * 2.0. */ -import { cloneDeep, findIndex } from 'lodash'; - import { kea, MakeLogicType } from 'kea'; +import { cloneDeep, findIndex } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { HttpLogic } from '../../../shared/http'; - import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; - +import { HttpLogic } from '../../../shared/http'; +import { AppLogic } from '../../app_logic'; import { Connector, ContentSourceDetails, ContentSourceStatus, SourceDataItem } from '../../types'; import { staticSourceData } from './source_data'; -import { AppLogic } from '../../app_logic'; - interface ServerStatuses { [key: string]: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx index b1a6ea128ac8c..2438061c67759 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx @@ -10,10 +10,10 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - import { Route, Switch, Redirect } from 'react-router-dom'; +import { shallow } from 'enzyme'; + import { ADD_SOURCE_PATH, PERSONAL_SOURCES_PATH, SOURCES_PATH, getSourcesPath } from '../../routes'; import { SourcesRouter } from './sources_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index 28ad2fe3a3965..dcc15be4462c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -6,16 +6,16 @@ */ import React, { useEffect } from 'react'; +import { Redirect, Route, Switch, useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions, useValues } from 'kea'; -import { Redirect, Route, Switch, useLocation } from 'react-router-dom'; +import { LicensingLogic } from '../../../../applications/shared/licensing'; +import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; - -import { LicensingLogic } from '../../../../applications/shared/licensing'; - +import { AppLogic } from '../../app_logic'; import { NAV } from '../../constants'; import { ADD_SOURCE_PATH, @@ -26,17 +26,13 @@ import { getSourcesPath, } from '../../routes'; -import { FlashMessages } from '../../../shared/flash_messages'; - -import { AppLogic } from '../../app_logic'; -import { staticSourceData } from './source_data'; -import { SourcesLogic } from './sources_logic'; - import { AddSource, AddSourceList } from './components/add_source'; import { SourceAdded } from './components/source_added'; import { OrganizationSources } from './organization_sources'; import { PrivateSources } from './private_sources'; +import { staticSourceData } from './source_data'; import { SourceRouter } from './source_router'; +import { SourcesLogic } from './sources_logic'; import './sources.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx index 742d19ebbd156..06d7ecff50299 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx @@ -9,10 +9,10 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; -import { shallow } from 'enzyme'; - import React from 'react'; +import { shallow } from 'enzyme'; + import { EuiModal } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx index ac70d74cc3d78..c62f0b00258d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx @@ -9,9 +9,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiButton, EuiLink, @@ -25,10 +22,11 @@ import { EuiOverlayMask, EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { Loading } from '../../../shared/loading'; import { SourceIcon } from '../../components/shared/source_icon'; - import { EXTERNAL_IDENTITIES_DOCS_URL, DOCUMENT_PERMISSIONS_DOCS_URL } from '../../routes'; import { @@ -36,7 +34,6 @@ import { DOCUMENT_PERMISSIONS_LINK, UNDERSTAND_BUTTON, } from './constants'; - import { SourcesLogic } from './sources_logic'; interface SourcesViewProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx index 0408bbf3e7e84..a8fcdfd7cb257 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ErrorStatePrompt } from '../../../shared/error_state'; + import { ErrorState } from './'; describe('ErrorState', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx index 74e52912b551b..8116d55542820 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx @@ -8,6 +8,7 @@ // TODO: Remove EuiPage & EuiPageBody before exposing full app import React from 'react'; + import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts index 3df7fbb5a0596..0e072210d2489 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { ContentSource, User, Group } from '../../../types'; - import { DEFAULT_META } from '../../../../shared/constants'; +import { ContentSource, User, Group } from '../../../types'; export const mockGroupsValues = { groups: [] as Group[], diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx index 1065c2c2df4c3..26ac5e484f0d7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx @@ -8,12 +8,13 @@ import { setMockValues, setMockActions } from '../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; -import { AddGroupModal } from './add_group_modal'; +import { shallow } from 'enzyme'; import { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import { AddGroupModal } from './add_group_modal'; + describe('AddGroupModal', () => { const closeNewGroupModal = jest.fn(); const saveNewGroup = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx index f49c978d06e90..fb82e9393f2a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiButton, @@ -22,9 +21,9 @@ import { EuiModalHeaderTitle, EuiOverlayMask, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { CANCEL_BUTTON } from '../../../constants'; - import { GroupsLogic } from '../groups_logic'; const ADD_GROUP_HEADER = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx index 2dffe89f38569..9118bc5e7adf3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx @@ -8,12 +8,13 @@ import { setMockActions } from '../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; -import { ClearFiltersLink } from './clear_filters_link'; +import { shallow } from 'enzyme'; import { EuiLink } from '@elastic/eui'; +import { ClearFiltersLink } from './clear_filters_link'; + describe('ClearFiltersLink', () => { const resetGroupsFilters = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.tsx index 3734148ea3afa..6aeb2241bca61 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { GroupsLogic } from '../groups_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx index 965a4887f4359..a460070772d1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx @@ -8,14 +8,15 @@ import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiFieldSearch, EuiFilterSelectItem, EuiCard, EuiPopoverTitle } from '@elastic/eui'; -import { FilterableUsersList } from './filterable_users_list'; - import { User } from '../../../types'; +import { FilterableUsersList } from './filterable_users_list'; + const mockSetState = jest.fn(); const useStateMock: any = (initState: any) => [initState, mockSetState]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.tsx index ef222e934260b..8a7875b5e8310 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.tsx @@ -7,8 +7,6 @@ import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; - import { EuiCard, EuiFieldSearch, @@ -17,6 +15,7 @@ import { EuiPopoverTitle, EuiSpacer, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { User } from '../../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx index 36a99425c9793..1813b766b9875 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx @@ -9,13 +9,14 @@ import { setMockActions } from '../../../../__mocks__'; import { users } from '../../../__mocks__/users.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { FilterableUsersPopover } from './filterable_users_popover'; -import { FilterableUsersList } from './filterable_users_list'; +import { shallow } from 'enzyme'; import { EuiFilterGroup, EuiPopover } from '@elastic/eui'; +import { FilterableUsersList } from './filterable_users_list'; +import { FilterableUsersPopover } from './filterable_users_popover'; + const closePopover = jest.fn(); const addFilteredUser = jest.fn(); const removeFilteredUser = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx index b47232197c47f..3cf4d97c781d9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx @@ -12,8 +12,8 @@ import { useActions } from 'kea'; import { EuiFilterGroup, EuiPopover } from '@elastic/eui'; import { User } from '../../../types'; - import { GroupsLogic } from '../groups_logic'; + import { FilterableUsersList } from './filterable_users_list'; interface FilterableUsersPopoverProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx index 8ee14b7c82cc4..949ae9d502e73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx @@ -6,16 +6,17 @@ */ import { setMockValues } from '../../../../__mocks__'; -import { groups } from '../../../__mocks__/groups.mock'; import { contentSources } from '../../../__mocks__/content_sources.mock'; +import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { GroupManagerModal } from './group_manager_modal'; +import { shallow } from 'enzyme'; import { EuiOverlayMask, EuiModal, EuiEmptyPrompt } from '@elastic/eui'; +import { GroupManagerModal } from './group_manager_modal'; + const hideModal = jest.fn(); const selectAll = jest.fn(); const saveItems = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx index ae5c042fc27dc..b4317ed9bd417 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiButton, EuiButtonEmpty, @@ -26,15 +24,13 @@ import { EuiOverlayMask, EuiSpacer, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; - -import { Group } from '../../../types'; +import noSharedSourcesIcon from '../../../assets/share_circle.svg'; import { CANCEL_BUTTON } from '../../../constants'; import { SOURCES_PATH } from '../../../routes'; - -import noSharedSourcesIcon from '../../../assets/share_circle.svg'; - +import { Group } from '../../../types'; import { GroupLogic } from '../group_logic'; import { GroupsLogic } from '../groups_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx index ea49ae09f3a25..e39d72a861b6f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx @@ -9,21 +9,22 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; + import { shallow } from 'enzyme'; +import { EuiFieldText } from '@elastic/eui'; + +import { Loading } from '../../../../shared/loading'; +import { ContentSection } from '../../../components/shared/content_section'; +import { SourcesTable } from '../../../components/shared/sources_table'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; + import { GroupOverview, EMPTY_SOURCES_DESCRIPTION, EMPTY_USERS_DESCRIPTION, } from './group_overview'; -import { ContentSection } from '../../../components/shared/content_section'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { SourcesTable } from '../../../components/shared/sources_table'; -import { Loading } from '../../../../shared/loading'; - -import { EuiFieldText } from '@elastic/eui'; - const deleteGroup = jest.fn(); const showSharedSourcesModal = jest.fn(); const showManageUsersModal = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index ca67c2aac98ad..df9c0b5db9b7d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiButton, EuiConfirmModal, @@ -22,20 +20,19 @@ import { EuiSpacer, EuiHorizontalRule, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { CANCEL_BUTTON } from '../../../constants'; - -import { AppLogic } from '../../../app_logic'; +import { Loading } from '../../../../shared/loading'; import { TruncatedContent } from '../../../../shared/truncate'; +import { AppLogic } from '../../../app_logic'; import { ContentSection } from '../../../components/shared/content_section'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { Loading } from '../../../../shared/loading'; import { SourcesTable } from '../../../components/shared/sources_table'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { CANCEL_BUTTON } from '../../../constants'; +import { GroupLogic, MAX_NAME_LENGTH } from '../group_logic'; import { GroupUsersTable } from './group_users_table'; -import { GroupLogic, MAX_NAME_LENGTH } from '../group_logic'; - export const EMPTY_SOURCES_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx index 19898172fb4e7..205eafd69cd10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx @@ -9,14 +9,15 @@ import { setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import moment from 'moment'; +import { EuiTableRow } from '@elastic/eui'; + import { GroupRow, NO_USERS_MESSAGE, NO_SOURCES_MESSAGE } from './group_row'; import { GroupUsers } from './group_users'; -import { EuiTableRow } from '@elastic/eui'; - describe('GroupRow', () => { beforeEach(() => { setMockValues({ isFederatedAuth: true }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx index 1a085aea93cc6..5e89d4491d597 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx @@ -6,21 +6,20 @@ */ import React from 'react'; -import moment from 'moment'; -import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; +import { useValues } from 'kea'; +import moment from 'moment'; import { EuiTableRow, EuiTableRowCell, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { TruncatedContent } from '../../../../shared/truncate'; import { EuiLinkTo } from '../../../../shared/react_router_helpers'; - -import { Group } from '../../../types'; - +import { TruncatedContent } from '../../../../shared/truncate'; import { AppLogic } from '../../../app_logic'; import { getGroupPath } from '../../../routes'; +import { Group } from '../../../types'; import { MAX_NAME_LENGTH } from '../group_logic'; + import { GroupSources } from './group_sources'; import { GroupUsers } from './group_users'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx index e4c626a28c1a6..23c00d0fa209e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx @@ -8,13 +8,14 @@ import { contentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; +import { EuiFilterGroup } from '@elastic/eui'; + import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; import { SourceOptionItem } from './source_option_item'; -import { EuiFilterGroup } from '@elastic/eui'; - const onButtonClick = jest.fn(); const closePopover = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.tsx index a8f8c18cc6f38..77d7de91caf7c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.tsx @@ -7,9 +7,8 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; - import { EuiFilterGroup, EuiPopover, EuiPopoverTitle, EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ContentSource } from '../../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx index 7dae74155d0d6..e75b325a4eae9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx @@ -9,12 +9,13 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow, mount } from 'enzyme'; import { EuiLoadingContent, EuiButtonEmpty } from '@elastic/eui'; -import { GroupRowUsersDropdown } from './group_row_users_dropdown'; import { FilterableUsersPopover } from './filterable_users_popover'; +import { GroupRowUsersDropdown } from './group_row_users_dropdown'; const fetchGroupUsers = jest.fn(); const onButtonClick = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.tsx index 9ca9c8339ba6a..aaf715fc71615 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.tsx @@ -12,6 +12,7 @@ import { useActions, useValues } from 'kea'; import { EuiLoadingContent, EuiButtonEmpty } from '@elastic/eui'; import { GroupsLogic } from '../groups_logic'; + import { FilterableUsersPopover } from './filterable_users_popover'; interface GroupRowUsersDropdownProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx index 49305ec33d228..4a9244486bf30 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx @@ -9,14 +9,15 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; + import { shallow } from 'enzyme'; +import { EuiTable, EuiEmptyPrompt, EuiRange } from '@elastic/eui'; + import { Loading } from '../../../../shared/loading'; import { GroupSourcePrioritization } from './group_source_prioritization'; -import { EuiTable, EuiEmptyPrompt, EuiRange } from '@elastic/eui'; - const updatePriority = jest.fn(); const saveGroupSourcePrioritization = jest.fn(); const showSharedSourcesModal = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx index 6907618e40b46..9b131e730b937 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx @@ -9,8 +9,6 @@ import React, { ChangeEvent, MouseEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiButton, EuiEmptyPrompt, @@ -26,14 +24,13 @@ import { EuiTableRow, EuiTableRowCell, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Loading } from '../../../../shared/loading'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { SourceIcon } from '../../../components/shared/source_icon'; - -import { GroupLogic } from '../group_logic'; - +import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { ContentSource } from '../../../types'; +import { GroupLogic } from '../group_logic'; const HEADER_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.headerTitle', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx index fd2a5e2bc6d9a..a245f0a768b0e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx @@ -8,15 +8,15 @@ import { contentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { GroupSources } from './group_sources'; -import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; +import { shallow } from 'enzyme'; import { SourceIcon } from '../../../components/shared/source_icon'; - import { ContentSourceDetails } from '../../../types'; +import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; +import { GroupSources } from './group_sources'; + describe('GroupSources', () => { it('renders', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx index ae3b5000941b1..97739e46caba4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx @@ -9,7 +9,6 @@ import React, { useState } from 'react'; import { SourceIcon } from '../../../components/shared/source_icon'; import { MAX_TABLE_ROW_ICONS } from '../../../constants'; - import { ContentSource } from '../../../types'; import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx index ead4af451ee7a..e4dde81949bfa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx @@ -8,12 +8,13 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; -import { GroupSubNav } from './group_sub_nav'; +import { shallow } from 'enzyme'; import { SideNavLink } from '../../../../shared/layout'; +import { GroupSubNav } from './group_sub_nav'; + describe('GroupSubNav', () => { it('renders empty when no group id present', () => { setMockValues({ group: {} }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx index e2bd6e8ae91f2..c5fc0717d1105 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx @@ -6,14 +6,13 @@ */ import React from 'react'; -import { useValues } from 'kea'; -import { GroupLogic } from '../group_logic'; -import { NAV } from '../../../constants'; +import { useValues } from 'kea'; import { SideNavLink } from '../../../../shared/layout'; - +import { NAV } from '../../../constants'; import { getGroupPath, getGroupSourcePrioritizationPath } from '../../../routes'; +import { GroupLogic } from '../group_logic'; export const GroupSubNav: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx index f1bc063e1a223..eba79ea70177d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx @@ -8,14 +8,14 @@ import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow } from 'enzyme'; +import { UserIcon } from '../../../components/shared/user_icon'; import { User } from '../../../types'; -import { GroupUsers } from './group_users'; import { GroupRowUsersDropdown } from './group_row_users_dropdown'; - -import { UserIcon } from '../../../components/shared/user_icon'; +import { GroupUsers } from './group_users'; const props = { groupUsers: users, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.tsx index 850910428c4b2..6e60df15ed30a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.tsx @@ -9,7 +9,6 @@ import React, { useState } from 'react'; import { UserIcon } from '../../../components/shared/user_icon'; import { MAX_TABLE_ROW_ICONS } from '../../../constants'; - import { User } from '../../../types'; import { GroupRowUsersDropdown } from './group_row_users_dropdown'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx index 83e945547438f..a6376d7653627 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx @@ -9,14 +9,15 @@ import { setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { User } from '../../../types'; +import { EuiTable, EuiTablePagination } from '@elastic/eui'; -import { GroupUsersTable } from './group_users_table'; import { TableHeader } from '../../../../shared/table_header'; +import { User } from '../../../types'; -import { EuiTable, EuiTablePagination } from '@elastic/eui'; +import { GroupUsersTable } from './group_users_table'; const group = groups[0]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx index 4b337fda9143d..5d070b1a21b7d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx @@ -9,17 +9,14 @@ import React, { useState } from 'react'; import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiTable, EuiTableBody, EuiTablePagination } from '@elastic/eui'; import { Pager } from '@elastic/eui'; - -import { User } from '../../../types'; +import { i18n } from '@kbn/i18n'; import { TableHeader } from '../../../../shared/table_header'; -import { UserRow } from '../../../components/shared/user_row'; - import { AppLogic } from '../../../app_logic'; +import { UserRow } from '../../../components/shared/user_row'; +import { User } from '../../../types'; import { GroupLogic } from '../group_logic'; const USERS_PER_PAGE = 10; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx index d6724499490cf..f60a13ec296d5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx @@ -8,18 +8,18 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; -import { DEFAULT_META } from '../../../../shared/constants'; - import React from 'react'; + import { shallow } from 'enzyme'; +import { EuiTable, EuiTableHeaderCell } from '@elastic/eui'; + +import { DEFAULT_META } from '../../../../shared/constants'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; -import { GroupsTable } from './groups_table'; -import { GroupRow } from './group_row'; import { ClearFiltersLink } from './clear_filters_link'; - -import { EuiTable, EuiTableHeaderCell } from '@elastic/eui'; +import { GroupRow } from './group_row'; +import { GroupsTable } from './groups_table'; const setActivePage = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx index 31f549c3e2065..95292116cba05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiSpacer, EuiTable, @@ -18,14 +16,14 @@ import { EuiTableHeader, EuiTableHeaderCell, } from '@elastic/eui'; - -import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; +import { i18n } from '@kbn/i18n'; import { AppLogic } from '../../../app_logic'; +import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { GroupsLogic } from '../groups_logic'; -import { GroupRow } from './group_row'; import { ClearFiltersLink } from './clear_filters_link'; +import { GroupRow } from './group_row'; const GROUP_TABLE_HEADER = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx index 059dff969aee3..49d51dfc7254c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx @@ -9,11 +9,12 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { ManageUsersModal } from './manage_users_modal'; import { FilterableUsersList } from './filterable_users_list'; import { GroupManagerModal } from './group_manager_modal'; +import { ManageUsersModal } from './manage_users_modal'; const addGroupUser = jest.fn(); const removeGroupUser = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx index f937ded7d4918..dd72850a06ad9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx @@ -9,10 +9,11 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { SharedSourcesModal } from './shared_sources_modal'; import { GroupManagerModal } from './group_manager_modal'; +import { SharedSourcesModal } from './shared_sources_modal'; import { SourcesList } from './sources_list'; const group = groups[0]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx index d037a49875a7e..bad60e15ed2d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx @@ -8,14 +8,14 @@ import { contentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { SourceOptionItem } from './source_option_item'; +import { shallow } from 'enzyme'; import { TruncatedContent } from '../../../../shared/truncate'; - import { SourceIcon } from '../../../components/shared/source_icon'; +import { SourceOptionItem } from './source_option_item'; + describe('SourceOptionItem', () => { it('renders', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx index a87980415bd1f..e2da597a83598 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { TruncatedContent } from '../../../../shared/truncate'; - import { SourceIcon } from '../../../components/shared/source_icon'; import { ContentSource } from '../../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx index 56e700c10e04c..05fe2c92f9f72 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx @@ -8,12 +8,13 @@ import { contentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { SourcesList } from './sources_list'; +import { shallow } from 'enzyme'; import { EuiFilterSelectItem } from '@elastic/eui'; +import { SourcesList } from './sources_list'; + const addFilteredSource = jest.fn(); const removeFilteredSource = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx index b7efe84df180c..1e2a57da9ad2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx @@ -9,11 +9,11 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { contentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; +import { shallow } from 'enzyme'; import { SourcesList } from './sources_list'; +import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; const addFilteredSource = jest.fn(); const removeFilteredSource = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.tsx index b38d5fc55b6f8..5f75340d562ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiFilterButton, EuiFilterGroup, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { GroupsLogic } from '../groups_logic'; + import { SourcesList } from './sources_list'; const FILTER_SOURCES_BUTTON_TEXT = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx index 9eaaa64b1c4e4..e472563862015 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx @@ -9,10 +9,11 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { TableFilterUsersDropdown } from './table_filter_users_dropdown'; import { FilterableUsersPopover } from './filterable_users_popover'; +import { TableFilterUsersDropdown } from './table_filter_users_dropdown'; const closeFilterUsersDropdown = jest.fn(); const toggleFilterUsersDropdown = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx index 9ddb955767c14..c09e1e3cf87cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiFilterButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { GroupsLogic } from '../groups_logic'; + import { FilterableUsersPopover } from './filterable_users_popover'; const FILTER_USERS_BUTTON_TEXT = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx index 0fdaf74376494..bcc58c394b516 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx @@ -8,13 +8,14 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; -import { TableFilters } from './table_filters'; +import { shallow } from 'enzyme'; import { EuiFieldSearch } from '@elastic/eui'; + import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; import { TableFilterUsersDropdown } from './table_filter_users_dropdown'; +import { TableFilters } from './table_filters'; const setFilterValue = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx index cfd40e1a0df4e..e9ea6a7c6b4aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx @@ -9,9 +9,8 @@ import React, { ChangeEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { AppLogic } from '../../../app_logic'; import { GroupsLogic } from '../groups_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx index 01f67cc910afd..6c8dbbde2e69f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx @@ -8,12 +8,14 @@ import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { UserOptionItem } from './user_option_item'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + import { UserIcon } from '../../../components/shared/user_icon'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { UserOptionItem } from './user_option_item'; const user = users[0]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts index d8d41b5e2888a..836efa82995fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts @@ -11,15 +11,15 @@ import { mockFlashMessageHelpers, mockHttpValues, } from '../../../__mocks__'; +import { groups } from '../../__mocks__/groups.mock'; import { nextTick } from '@kbn/test/jest'; -import { groups } from '../../__mocks__/groups.mock'; +import { GROUPS_PATH } from '../../routes'; + import { mockGroupValues } from './__mocks__/group_logic.mock'; import { GroupLogic } from './group_logic'; -import { GROUPS_PATH } from '../../routes'; - describe('GroupLogic', () => { const { mount } = new LogicMounter(GroupLogic); const { http } = mockHttpValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts index 7e7ce838434f5..f23b182b98649 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts @@ -7,10 +7,9 @@ import { kea, MakeLogicType } from 'kea'; import { isEqual } from 'lodash'; + import { i18n } from '@kbn/i18n'; -import { HttpLogic } from '../../../shared/http'; -import { KibanaLogic } from '../../../shared/kibana'; import { clearFlashMessages, flashAPIErrors, @@ -18,9 +17,9 @@ import { setQueuedSuccessMessage, setQueuedErrorMessage, } from '../../../shared/flash_messages'; - +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import { GROUPS_PATH } from '../../routes'; - import { ContentSourceDetails, GroupDetails, User, SourcePriority } from '../../types'; export const MAX_NAME_LENGTH = 40; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx index a04fc4c744790..0b218f2496154 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx @@ -7,25 +7,21 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; +import { groups } from '../../__mocks__/groups.mock'; import React from 'react'; -import { shallow } from 'enzyme'; - import { Route, Switch } from 'react-router-dom'; -import { groups } from '../../__mocks__/groups.mock'; +import { shallow } from 'enzyme'; +import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { GroupOverview } from './components/group_overview'; import { GroupSourcePrioritization } from './components/group_source_prioritization'; - -import { GroupRouter } from './group_router'; - -import { FlashMessages } from '../../../shared/flash_messages'; - import { ManageUsersModal } from './components/manage_users_modal'; import { SharedSourcesModal } from './components/shared_sources_modal'; +import { GroupRouter } from './group_router'; describe('GroupRouter', () => { const initializeGroup = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx index 82eb7931dfcdc..a5b8bd138d0c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx @@ -6,23 +6,21 @@ */ import React, { useEffect } from 'react'; +import { Route, Switch, useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { Route, Switch, useParams } from 'react-router-dom'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; - -import { GROUP_SOURCE_PRIORITIZATION_PATH, GROUP_PATH } from '../../routes'; import { NAV } from '../../constants'; -import { GroupLogic } from './group_logic'; - -import { ManageUsersModal } from './components/manage_users_modal'; -import { SharedSourcesModal } from './components/shared_sources_modal'; +import { GROUP_SOURCE_PRIORITIZATION_PATH, GROUP_PATH } from '../../routes'; import { GroupOverview } from './components/group_overview'; import { GroupSourcePrioritization } from './components/group_source_prioritization'; +import { ManageUsersModal } from './components/manage_users_modal'; +import { SharedSourcesModal } from './components/shared_sources_modal'; +import { GroupLogic } from './group_logic'; export const GroupRouter: React.FC = () => { const { groupId } = useParams() as { groupId: string }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx index d67dd5857561e..8470c5d3e0f66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx @@ -11,23 +11,22 @@ import { groups } from '../../__mocks__/groups.mock'; import { meta } from '../../__mocks__/meta.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { Groups } from './groups'; +import { EuiFieldSearch, EuiLoadingSpinner } from '@elastic/eui'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { Loading } from '../../../shared/loading'; +import { DEFAULT_META } from '../../../shared/constants'; import { FlashMessages } from '../../../shared/flash_messages'; +import { Loading } from '../../../shared/loading'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; import { AddGroupModal } from './components/add_group_modal'; import { ClearFiltersLink } from './components/clear_filters_link'; import { GroupsTable } from './components/groups_table'; import { TableFilters } from './components/table_filters'; - -import { DEFAULT_META } from '../../../shared/constants'; - -import { EuiFieldSearch, EuiLoadingSpinner } from '@elastic/eui'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { Groups } from './groups'; const getSearchResults = jest.fn(); const openNewGroupModal = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx index 7a8b9343691f9..b2bf0364b2d1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx @@ -8,26 +8,22 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; - -import { AppLogic } from '../../app_logic'; +import { i18n } from '@kbn/i18n'; +import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages'; import { Loading } from '../../../shared/loading'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { AppLogic } from '../../app_logic'; import { ViewContentHeader } from '../../components/shared/view_content_header'; - import { getGroupPath, USERS_PATH } from '../../routes'; -import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages'; - -import { GroupsLogic } from './groups_logic'; - import { AddGroupModal } from './components/add_group_modal'; import { ClearFiltersLink } from './components/clear_filters_link'; import { GroupsTable } from './components/groups_table'; import { TableFilters } from './components/table_filters'; +import { GroupsLogic } from './groups_logic'; export const Groups: React.FC = () => { const { messages } = useValues(FlashMessagesLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts index 26d7f9784cc6e..806c6e1c69f84 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts @@ -6,15 +6,15 @@ */ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; +import { contentSources } from '../../__mocks__/content_sources.mock'; +import { groups } from '../../__mocks__/groups.mock'; +import { users } from '../../__mocks__/users.mock'; import { nextTick } from '@kbn/test/jest'; -import { DEFAULT_META } from '../../../shared/constants'; import { JSON_HEADER as headers } from '../../../../../common/constants'; +import { DEFAULT_META } from '../../../shared/constants'; -import { groups } from '../../__mocks__/groups.mock'; -import { contentSources } from '../../__mocks__/content_sources.mock'; -import { users } from '../../__mocks__/users.mock'; import { mockGroupsValues } from './__mocks__/groups_logic.mock'; import { GroupsLogic } from './groups_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts index 68a6eb7bdf344..a036cdda3d68e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts @@ -6,22 +6,20 @@ */ import { kea, MakeLogicType } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { HttpLogic } from '../../../shared/http'; +import { i18n } from '@kbn/i18n'; +import { JSON_HEADER as headers } from '../../../../../common/constants'; +import { Meta } from '../../../../../common/types'; +import { DEFAULT_META } from '../../../shared/constants'; import { clearFlashMessages, flashAPIErrors, setSuccessMessage, } from '../../../shared/flash_messages'; - +import { HttpLogic } from '../../../shared/http'; import { ContentSource, Group, User } from '../../types'; -import { JSON_HEADER as headers } from '../../../../../common/constants'; -import { DEFAULT_META } from '../../../shared/constants'; -import { Meta } from '../../../../../common/types'; - export const MAX_NAME_LENGTH = 40; interface GroupsServerData { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx index 43c31038a45c6..0295605eddd4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx @@ -9,14 +9,13 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockActions } from '../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - import { Route, Switch } from 'react-router-dom'; -import { GroupsRouter } from './groups_router'; +import { shallow } from 'enzyme'; import { GroupRouter } from './group_router'; import { Groups } from './groups'; +import { GroupsRouter } from './groups_router'; describe('GroupsRouter', () => { const initializeGroups = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx index e835a2668f3d3..d8c4f4801ba24 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx @@ -6,21 +6,18 @@ */ import React, { useEffect } from 'react'; +import { Route, Switch } from 'react-router-dom'; import { useActions } from 'kea'; -import { Route, Switch } from 'react-router-dom'; - import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; - -import { GROUP_PATH, GROUPS_PATH } from '../../routes'; import { NAV } from '../../constants'; - -import { GroupsLogic } from './groups_logic'; +import { GROUP_PATH, GROUPS_PATH } from '../../routes'; import { GroupRouter } from './group_router'; import { Groups } from './groups'; +import { GroupsLogic } from './groups_logic'; import './groups.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts index f03dcfe98ddd0..787354974cb31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { setMockValues as setMockKeaValues, setMockActions } from '../../../../__mocks__/kea.mock'; import { DEFAULT_INITIAL_APP_DATA } from '../../../../../../common/__mocks__'; +import { setMockValues as setMockKeaValues, setMockActions } from '../../../../__mocks__/kea.mock'; const { workplaceSearch: mockAppValues } = DEFAULT_INITIAL_APP_DATA; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx index 8f962ec4cf665..68dece976a09c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx @@ -10,6 +10,7 @@ import '../../../__mocks__/enterprise_search_url.mock'; import { mockTelemetryActions } from '../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton, EuiButtonEmpty } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx index 68a4c4dc10f4f..2f8d06b71fc27 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { useActions } from 'kea'; import { @@ -20,8 +21,8 @@ import { EuiLinkProps, } from '@elastic/eui'; -import { TelemetryLogic } from '../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; +import { TelemetryLogic } from '../../../shared/telemetry'; interface OnboardingCardProps { title: React.ReactNode; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx index 7f676ce2faac2..7a368e7d384ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx @@ -6,16 +6,18 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; + import './__mocks__/overview_logic.mock'; -import { setMockValues } from './__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { SOURCES_PATH, USERS_PATH } from '../../routes'; -import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; +import { setMockValues } from './__mocks__'; import { OnboardingCard } from './onboarding_card'; +import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; const account = { id: '1', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index ae30a52c1541c..fc3998fcdfeec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; + import { useValues, useActions } from 'kea'; import { @@ -22,17 +21,18 @@ import { EuiButtonEmptyProps, EuiLinkProps, } from '@elastic/eui'; -import sharedSourcesIcon from '../../components/shared/assets/source_icons/share_circle.svg'; -import { TelemetryLogic } from '../../../shared/telemetry'; -import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; -import { SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; - -import { ContentSection } from '../../components/shared/content_section'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; +import { TelemetryLogic } from '../../../shared/telemetry'; import { AppLogic } from '../../app_logic'; -import { OverviewLogic } from './overview_logic'; +import sharedSourcesIcon from '../../components/shared/assets/source_icons/share_circle.svg'; +import { ContentSection } from '../../components/shared/content_section'; +import { SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; import { OnboardingCard } from './onboarding_card'; +import { OverviewLogic } from './overview_logic'; const SOURCES_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.overviewOnboardingSourcesCard.title', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx index cf4f96f6b788b..412977f18fadf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx @@ -6,12 +6,14 @@ */ import './__mocks__/overview_logic.mock'; -import { setMockValues } from './__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlexGrid } from '@elastic/eui'; +import { setMockValues } from './__mocks__'; import { OrganizationStats } from './organization_stats'; import { StatisticCard } from './statistic_card'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx index 52c370caac989..525035030b8cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx @@ -6,18 +6,18 @@ */ import React from 'react'; -import { EuiFlexGrid } from '@elastic/eui'; + import { useValues } from 'kea'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGrid } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AppLogic } from '../../app_logic'; import { ContentSection } from '../../components/shared/content_section'; import { SOURCES_PATH, USERS_PATH } from '../../routes'; -import { AppLogic } from '../../app_logic'; import { OverviewLogic } from './overview_logic'; - import { StatisticCard } from './statistic_card'; export const OrganizationStats: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx index fc70a07e339e4..2ec2d949ff491 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx @@ -7,18 +7,19 @@ import '../../../__mocks__/react_router_history.mock'; import './__mocks__/overview_logic.mock'; -import { mockActions, setMockValues } from './__mocks__'; import React from 'react'; + import { shallow, mount } from 'enzyme'; import { Loading } from '../../../shared/loading'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { mockActions, setMockValues } from './__mocks__'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; -import { RecentActivity } from './recent_activity'; import { Overview } from './overview'; +import { RecentActivity } from './recent_activity'; describe('Overview', () => { describe('non-happy-path states', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx index 07bc999922661..6bf84b585da80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx @@ -8,22 +8,22 @@ // TODO: Remove EuiPage & EuiPageBody before exposing full app import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useActions, useValues } from 'kea'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; - import { AppLogic } from '../../app_logic'; -import { OverviewLogic } from './overview_logic'; - -import { Loading } from '../../../shared/loading'; import { ProductButton } from '../../components/shared/product_button'; import { ViewContentHeader } from '../../components/shared/view_content_header'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; +import { OverviewLogic } from './overview_logic'; import { RecentActivity } from './recent_activity'; const ONBOARDING_HEADER_TITLE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts index 6d0beb638cd52..75513cfba3a09 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts @@ -6,6 +6,7 @@ */ import { kea, MakeLogicType } from 'kea'; + import { HttpLogic } from '../../../shared/http'; import { FeedActivity } from './recent_activity'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx index 9c571bd8bc169..0b62207afc520 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx @@ -6,15 +6,17 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; + import './__mocks__/overview_logic.mock'; -import { setMockValues } from './__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { setMockValues } from './__mocks__'; import { RecentActivity, RecentActivityItem } from './recent_activity'; const organization = { name: 'foo', defaultOrgName: 'bar' }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index 1dcec989a94c7..43d3f880feef4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -7,19 +7,19 @@ import React from 'react'; -import moment from 'moment'; import { useValues, useActions } from 'kea'; +import moment from 'moment'; import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiSpacer, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ContentSection } from '../../components/shared/content_section'; -import { TelemetryLogic } from '../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; -import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; +import { TelemetryLogic } from '../../../shared/telemetry'; +import { AppLogic } from '../../app_logic'; +import { ContentSection } from '../../components/shared/content_section'; import { RECENT_ACTIVITY_TITLE } from '../../constants'; +import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; -import { AppLogic } from '../../app_logic'; import { OverviewLogic } from './overview_logic'; import './recent_activity.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx index 2893c3630393e..ff1d69e406830 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx @@ -8,6 +8,7 @@ import '../../../__mocks__/enterprise_search_url.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCard } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx index 83e6c2012a046..346debb1c5251 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiCard, EuiFlexItem, EuiTitle, EuiTextColor } from '@elastic/eui'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx index fb28fba9b3aea..4f7160ba631f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx @@ -8,7 +8,9 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiSwitch } from '@elastic/eui'; import { PrivateSourcesTable } from './private_sources_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx index 8ba29e5986e04..559b2fe3edbd1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx @@ -24,11 +24,10 @@ import { EuiTableRowCell, EuiSpacer, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { LicensingLogic } from '../../../../shared/licensing'; -import { SecurityLogic, PrivateSourceSection } from '../security_logic'; import { REMOTE_SOURCES_TOGGLE_TEXT, REMOTE_SOURCES_TABLE_DESCRIPTION, @@ -38,6 +37,7 @@ import { STANDARD_SOURCES_EMPTY_TABLE_TITLE, SOURCE, } from '../../../constants'; +import { SecurityLogic, PrivateSourceSection } from '../security_logic'; interface PrivateSourcesTableProps { sourceType: 'remote' | 'standard'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx index 24e6e5808355a..4eed6a6fefe68 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx @@ -9,11 +9,14 @@ import { setMockValues, setMockActions } from '../../../__mocks__'; import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiSwitch, EuiConfirmModal } from '@elastic/eui'; -import { Loading } from '../../../shared/loading'; +import { Loading } from '../../../shared/loading'; import { ViewContentHeader } from '../../components/shared/view_content_header'; + import { Security } from './security'; describe('Security', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx index 818dd34447c73..ba1ffb66f4691 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx @@ -23,15 +23,11 @@ import { EuiOverlayMask, } from '@elastic/eui'; -import { LicensingLogic } from '../../../shared/licensing'; import { FlashMessages } from '../../../shared/flash_messages'; -import { LicenseCallout } from '../../components/shared/license_callout'; +import { LicensingLogic } from '../../../shared/licensing'; import { Loading } from '../../../shared/loading'; +import { LicenseCallout } from '../../components/shared/license_callout'; import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { SecurityLogic } from './security_logic'; - -import { PrivateSourcesTable } from './components/private_sources_table'; - import { SECURITY_UNSAVED_CHANGES_MESSAGE, RESET_BUTTON, @@ -46,6 +42,9 @@ import { PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT, } from '../../constants'; +import { PrivateSourcesTable } from './components/private_sources_table'; +import { SecurityLogic } from './security_logic'; + export const Security: React.FC = () => { const [confirmModalVisible, setConfirmModalVisibility] = useState(false); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts index c2bd1be390592..02d8fdd3c30e4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts @@ -5,11 +5,13 @@ * 2.0. */ -import { LogicMounter } from '../../../__mocks__/kea.mock'; import { mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; -import { SecurityLogic } from './security_logic'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; + import { nextTick } from '@kbn/test/jest'; +import { SecurityLogic } from './security_logic'; + describe('SecurityLogic', () => { const { http } = mockHttpValues; const { flashAPIErrors } = mockFlashMessageHelpers; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts index 8689cec037848..07ebec41366b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts @@ -5,11 +5,10 @@ * 2.0. */ +import { kea, MakeLogicType } from 'kea'; import { cloneDeep } from 'lodash'; import { isEqual } from 'lodash'; -import { kea, MakeLogicType } from 'kea'; - import { clearFlashMessages, setSuccessMessage, @@ -17,7 +16,6 @@ import { } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { AppLogic } from '../../app_logic'; - import { SOURCE_RESTRICTIONS_SUCCESS_MESSAGE } from '../../constants'; export interface PrivateSource { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx index d1dd9e64c4d2d..13ef86a21a208 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx @@ -8,10 +8,10 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; - import { configuredSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { Loading } from '../../../../shared/loading'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx index 5b74f6d1d2806..9387cd4602255 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx @@ -19,12 +19,11 @@ import { EuiSpacer, } from '@elastic/eui'; -import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { Loading } from '../../../../shared/loading'; -import { SourceIcon } from '../../../components/shared/source_icon'; +import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { LicenseCallout } from '../../../components/shared/license_callout'; +import { SourceIcon } from '../../../components/shared/source_icon'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; - import { CONFIGURE_BUTTON, CONNECTORS_HEADER_TITLE, @@ -36,9 +35,7 @@ import { } from '../../../constants'; import { getSourcesPath } from '../../../routes'; import { SourceDataItem } from '../../../types'; - import { staticSourceData } from '../../content_sources/source_data'; - import { SettingsLogic } from '../settings_logic'; export const Connectors: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx index 8f77c229ad6f8..ed05829d9e082 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx @@ -10,6 +10,7 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiFieldText } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx index d57621bd397db..37f9e288f7f3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx @@ -11,16 +11,14 @@ import { useActions, useValues } from 'kea'; import { EuiButton, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { ContentSection } from '../../../components/shared/content_section'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { CUSTOMIZE_HEADER_TITLE, CUSTOMIZE_HEADER_DESCRIPTION, CUSTOMIZE_NAME_LABEL, CUSTOMIZE_NAME_BUTTON, } from '../../../constants'; - -import { ContentSection } from '../../../components/shared/content_section'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; - import { SettingsLogic } from '../settings_logic'; export const Customize: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx index 6fc9d51f42a86..55a58610e0ed6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx @@ -8,17 +8,18 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { oauthApplication } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiModal, EuiForm } from '@elastic/eui'; -import { oauthApplication } from '../../../__mocks__/content_sources.mock'; -import { OAUTH_DESCRIPTION, REDIRECT_INSECURE_ERROR_TEXT } from '../../../constants'; - import { CredentialItem } from '../../../components/shared/credential_item'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { OAUTH_DESCRIPTION, REDIRECT_INSECURE_ERROR_TEXT } from '../../../constants'; + import { OauthApplication } from './oauth_application'; describe('OauthApplication', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx index 04759e4f5fdd0..28e7e2a33eaa1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx @@ -26,7 +26,11 @@ import { EuiText, } from '@elastic/eui'; -import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; +import { LicensingLogic } from '../../../../shared/licensing'; +import { ContentSection } from '../../../components/shared/content_section'; +import { CredentialItem } from '../../../components/shared/credential_item'; +import { LicenseBadge } from '../../../components/shared/license_badge'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { CLIENT_ID_LABEL, CLIENT_SECRET_LABEL, @@ -48,12 +52,7 @@ import { LICENSE_MODAL_DESCRIPTION, LICENSE_MODAL_LINK, } from '../../../constants'; - -import { LicensingLogic } from '../../../../shared/licensing'; -import { ContentSection } from '../../../components/shared/content_section'; -import { LicenseBadge } from '../../../components/shared/license_badge'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { CredentialItem } from '../../../components/shared/credential_item'; +import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; import { SettingsLogic } from '../settings_logic'; export const OauthApplication: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx index f00bb7d897e25..5cd8a3fc1cf03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SideNavLink } from '../../../../shared/layout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx index 20a6e349c1272..3f68997a17b8b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx @@ -7,10 +7,8 @@ import React from 'react'; -import { NAV } from '../../../constants'; - import { SideNavLink } from '../../../../shared/layout'; - +import { NAV } from '../../../constants'; import { ORG_SETTINGS_CUSTOMIZE_PATH, ORG_SETTINGS_CONNECTORS_PATH, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx index 73ea92117c6df..ed9f715fd6916 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx @@ -8,16 +8,17 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { sourceConfigData } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiConfirmModal } from '@elastic/eui'; -import { sourceConfigData } from '../../../__mocks__/content_sources.mock'; - import { Loading } from '../../../../shared/loading'; import { SaveConfig } from '../../content_sources/components/add_source/save_config'; + import { SourceConfig } from './source_config'; describe('SourceConfig', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx index 4b59e0f3401c5..4ed223931d6a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx @@ -8,18 +8,16 @@ import React, { useEffect, useState } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Loading } from '../../../../shared/loading'; import { SourceDataItem } from '../../../types'; -import { staticSourceData } from '../../content_sources/source_data'; -import { AddSourceLogic } from '../../content_sources/components/add_source/add_source_logic'; - import { AddSourceHeader } from '../../content_sources/components/add_source/add_source_header'; +import { AddSourceLogic } from '../../content_sources/components/add_source/add_source_logic'; import { SaveConfig } from '../../content_sources/components/add_source/save_config'; - +import { staticSourceData } from '../../content_sources/source_data'; import { SettingsLogic } from '../settings_logic'; interface SourceConfigProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts index b8b08b8658372..a57c2c1f9ad44 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts @@ -5,15 +5,14 @@ * 2.0. */ -import { LogicMounter } from '../../../__mocks__/kea.mock'; - import { mockFlashMessageHelpers, mockHttpValues, mockKibanaValues } from '../../../__mocks__'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; +import { configuredSources, oauthApplication } from '../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test/jest'; -import { configuredSources, oauthApplication } from '../../__mocks__/content_sources.mock'; - import { ORG_UPDATED_MESSAGE, OAUTH_APP_UPDATED_MESSAGE } from '../../constants'; + import { SettingsLogic } from './settings_logic'; describe('SettingsLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts index 5a4f366c737d5..ad552ff8f5a41 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts @@ -6,6 +6,7 @@ */ import { kea, MakeLogicType } from 'kea'; + import { i18n } from '@kbn/i18n'; import { @@ -14,13 +15,11 @@ import { setSuccessMessage, flashAPIErrors, } from '../../../shared/flash_messages'; -import { KibanaLogic } from '../../../shared/kibana'; import { HttpLogic } from '../../../shared/http'; - -import { Connector } from '../../types'; +import { KibanaLogic } from '../../../shared/kibana'; import { ORG_UPDATED_MESSAGE, OAUTH_APP_UPDATED_MESSAGE } from '../../constants'; - import { ORG_SETTINGS_CONNECTORS_PATH } from '../../routes'; +import { Connector } from '../../types'; interface IOauthApplication { name: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx index 7f3ba0a8f34b3..411414fb33eaf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx @@ -10,19 +10,17 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockActions } from '../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - import { Route, Redirect, Switch } from 'react-router-dom'; -import { staticSourceData } from '../content_sources/source_data'; +import { shallow } from 'enzyme'; import { FlashMessages } from '../../../shared/flash_messages'; +import { staticSourceData } from '../content_sources/source_data'; import { Connectors } from './components/connectors'; import { Customize } from './components/customize'; import { OauthApplication } from './components/oauth_application'; import { SourceConfig } from './components/source_config'; - import { SettingsRouter } from './settings_router'; describe('SettingsRouter', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx index ee9122b015eff..34dcc48621a2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx @@ -6,26 +6,23 @@ */ import React, { useEffect } from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; import { useActions } from 'kea'; -import { Redirect, Route, Switch } from 'react-router-dom'; +import { FlashMessages } from '../../../shared/flash_messages'; import { ORG_SETTINGS_PATH, ORG_SETTINGS_CUSTOMIZE_PATH, ORG_SETTINGS_CONNECTORS_PATH, ORG_SETTINGS_OAUTH_APPLICATION_PATH, } from '../../routes'; - -import { FlashMessages } from '../../../shared/flash_messages'; +import { staticSourceData } from '../content_sources/source_data'; import { Connectors } from './components/connectors'; import { Customize } from './components/customize'; import { OauthApplication } from './components/oauth_application'; import { SourceConfig } from './components/source_config'; - -import { staticSourceData } from '../content_sources/source_data'; - import { SettingsLogic } from './settings_logic'; export const SettingsRouter: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx index 8bec56603cd80..6b03e86080402 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx @@ -6,10 +6,12 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout } from '../../../shared/setup_guide'; + import { SetupGuide } from './'; describe('SetupGuide', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx index 810125fc931a6..13191f42bc566 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx @@ -6,18 +6,19 @@ */ import React from 'react'; + import { EuiSpacer, EuiTitle, EuiText, EuiButton, EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { DOCS_PREFIX } from '../../routes'; import GettingStarted from './assets/getting_started.png'; -import { DOCS_PREFIX } from '../../routes'; const GETTING_STARTED_LINK_URL = `${DOCS_PREFIX}/workplace-search-getting-started.html`; export const SetupGuide: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/index.ts b/x-pack/plugins/enterprise_search/public/index.ts index da343728b7d41..b7131e70fec07 100644 --- a/x-pack/plugins/enterprise_search/public/index.ts +++ b/x-pack/plugins/enterprise_search/public/index.ts @@ -6,6 +6,7 @@ */ import { PluginInitializerContext } from 'src/core/public'; + import { EnterpriseSearchPlugin } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => { diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index c10eb74f47720..f00e81a5accf7 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -12,15 +12,15 @@ import { HttpSetup, Plugin, PluginInitializerContext, -} from 'src/core/public'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; + DEFAULT_APP_CATEGORIES, +} from '../../../../src/core/public'; +import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; import { CloudSetup } from '../../cloud/public'; import { LicensingPluginStart } from '../../licensing/public'; -import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { APP_SEARCH_PLUGIN, diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts index 88cf30bb2a549..5c19ca7062b65 100644 --- a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts +++ b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { IRouter, KibanaRequest, RequestHandlerContext, RouteValidatorConfig, } from 'src/core/server'; +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; /** * Test helper that mocks Kibana's router and DRYs out various helper (callRoute, schema validation) diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts index c84254660a728..50ff082858fc8 100644 --- a/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts +++ b/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts @@ -6,6 +6,7 @@ */ import { loggingSystemMock } from 'src/core/server/mocks'; + import { ConfigType } from '../'; export const mockLogger = loggingSystemMock.createLogger().get(); diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts index 537e1b77f3e84..36ba2976f929a 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts @@ -6,6 +6,7 @@ */ import { get } from 'lodash'; + import { SavedObjectsServiceStart, Logger } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; diff --git a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts index 732dfbd02c10b..f71c8a5444c9c 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts @@ -6,6 +6,7 @@ */ import { get } from 'lodash'; + import { SavedObjectsServiceStart, Logger } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; diff --git a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts index 01210eba95368..e36ce94066789 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts @@ -6,6 +6,7 @@ */ import { get } from 'lodash'; + import { SavedObjectsServiceStart, Logger } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; diff --git a/x-pack/plugins/enterprise_search/server/index.ts b/x-pack/plugins/enterprise_search/server/index.ts index ac012077fdf84..c4552b9134eae 100644 --- a/x-pack/plugins/enterprise_search/server/index.ts +++ b/x-pack/plugins/enterprise_search/server/index.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; + import { EnterpriseSearchPlugin } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => { diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts index 4a978c66b16d6..3c5d33fa74d3b 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts @@ -5,14 +5,15 @@ * 2.0. */ +import { spacesMock } from '../../../spaces/server/mocks'; + +import { checkAccess } from './check_access'; + jest.mock('./enterprise_search_config_api', () => ({ callEnterpriseSearchConfigAPI: jest.fn(), })); import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; -import { checkAccess } from './check_access'; -import { spacesMock } from '../../../spaces/server/mocks'; - const enabledSpace = { id: 'space', name: 'space', diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.ts index 25c92d62c1203..0a5e0c9e2b832 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.ts @@ -6,9 +6,10 @@ */ import { KibanaRequest, Logger } from 'src/core/server'; -import { SpacesPluginStart } from '../../../spaces/server'; + import { SecurityPluginSetup } from '../../../security/server'; -import { ConfigType } from '../'; +import { SpacesPluginStart } from '../../../spaces/server'; +import { ConfigType } from '../index'; import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 61aeffd99db00..6c6744ef3e32b 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -5,14 +5,15 @@ * 2.0. */ +import { DEFAULT_INITIAL_APP_DATA } from '../../common/__mocks__'; + jest.mock('node-fetch'); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const fetchMock = require('node-fetch') as jest.Mock; +import fetch from 'node-fetch'; + const { Response } = jest.requireActual('node-fetch'); import { loggingSystemMock } from 'src/core/server/mocks'; -import { DEFAULT_INITIAL_APP_DATA } from '../../common/__mocks__'; import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; describe('callEnterpriseSearchConfigAPI', () => { @@ -101,7 +102,7 @@ describe('callEnterpriseSearchConfigAPI', () => { }); it('calls the config API endpoint', async () => { - fetchMock.mockImplementationOnce((url: string) => { + ((fetch as unknown) as jest.Mock).mockImplementationOnce((url: string) => { expect(url).toEqual('http://localhost:3002/api/ent/v2/internal/client_config'); return Promise.resolve(new Response(JSON.stringify(mockResponse))); }); @@ -117,7 +118,7 @@ describe('callEnterpriseSearchConfigAPI', () => { }); it('falls back without error when data is unavailable', async () => { - fetchMock.mockImplementationOnce((url: string) => Promise.resolve(new Response('{}'))); + ((fetch as unknown) as jest.Mock).mockReturnValueOnce(Promise.resolve(new Response('{}'))); expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({ access: { @@ -180,21 +181,17 @@ describe('callEnterpriseSearchConfigAPI', () => { const config = { host: '' }; expect(await callEnterpriseSearchConfigAPI({ ...mockDependencies, config })).toEqual({}); - expect(fetchMock).not.toHaveBeenCalled(); + expect(fetch).not.toHaveBeenCalled(); }); it('handles server errors', async () => { - fetchMock.mockImplementationOnce(() => { - return Promise.reject('500'); - }); + ((fetch as unknown) as jest.Mock).mockReturnValueOnce(Promise.reject('500')); expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({}); expect(mockDependencies.log.error).toHaveBeenCalledWith( 'Could not perform access check to Enterprise Search: 500' ); - fetchMock.mockImplementationOnce(() => { - return Promise.resolve('Bad Data'); - }); + ((fetch as unknown) as jest.Mock).mockReturnValueOnce(Promise.resolve('Bad Data')); expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({}); expect(mockDependencies.log.error).toHaveBeenCalledWith( 'Could not perform access check to Enterprise Search: TypeError: response.json is not a function' @@ -212,7 +209,7 @@ describe('callEnterpriseSearchConfigAPI', () => { ); // Timeout - fetchMock.mockImplementationOnce(async () => { + ((fetch as unknown) as jest.Mock).mockImplementationOnce(async () => { jest.advanceTimersByTime(250); return Promise.reject({ name: 'AbortError' }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 9f207361cef91..0ed4ad257f30b 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -9,11 +9,12 @@ import AbortController from 'abort-controller'; import fetch from 'node-fetch'; import { KibanaRequest, Logger } from 'src/core/server'; -import { ConfigType } from '../'; -import { Access } from './check_access'; -import { InitialAppData } from '../../common/types'; import { stripTrailingSlash } from '../../common/strip_slashes'; +import { InitialAppData } from '../../common/types'; +import { ConfigType } from '../index'; + +import { Access } from './check_access'; interface Params { request: KibanaRequest; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index 8d47ba0ec77ba..7199067a2c8f4 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -6,6 +6,7 @@ */ import { mockConfig, mockLogger } from '../__mocks__'; + import { JSON_HEADER, READ_ONLY_MODE_HEADER } from '../../common/constants'; import { EnterpriseSearchRequestHandler } from './enterprise_search_request_handler'; @@ -13,6 +14,7 @@ import { EnterpriseSearchRequestHandler } from './enterprise_search_request_hand jest.mock('node-fetch'); // eslint-disable-next-line @typescript-eslint/no-var-requires const fetchMock = require('node-fetch') as jest.Mock; + const { Response } = jest.requireActual('node-fetch'); const responseMock = { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index 39590b310fc26..f47df58c4eca1 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -7,6 +7,7 @@ import fetch, { Response } from 'node-fetch'; import querystring from 'querystring'; + import { RequestHandler, RequestHandlerContext, @@ -14,8 +15,9 @@ import { KibanaResponseFactory, Logger, } from 'src/core/server'; -import { ConfigType } from '../index'; + import { JSON_HEADER, READ_ONLY_MODE_HEADER } from '../../common/constants'; +import { ConfigType } from '../index'; interface ConstructorDependencies { config: ConfigType; diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 569479f921cdd..1b9659899097d 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -13,37 +13,39 @@ import { SavedObjectsServiceStart, IRouter, KibanaRequest, -} from 'src/core/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { SpacesPluginStart } from '../../spaces/server'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; -import { SecurityPluginSetup } from '../../security/server'; + DEFAULT_APP_CATEGORIES, +} from '../../../../src/core/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginStart } from '../../spaces/server'; import { ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; -import { ConfigType } from './'; + +import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; +import { registerTelemetryUsageCollector as registerESTelemetryUsageCollector } from './collectors/enterprise_search/telemetry'; +import { registerTelemetryUsageCollector as registerWSTelemetryUsageCollector } from './collectors/workplace_search/telemetry'; + import { checkAccess } from './lib/check_access'; import { EnterpriseSearchRequestHandler, IEnterpriseSearchRequestHandler, } from './lib/enterprise_search_request_handler'; -import { enterpriseSearchTelemetryType } from './saved_objects/enterprise_search/telemetry'; -import { registerTelemetryUsageCollector as registerESTelemetryUsageCollector } from './collectors/enterprise_search/telemetry'; -import { registerTelemetryRoute } from './routes/enterprise_search/telemetry'; +import { registerAppSearchRoutes } from './routes/app_search'; import { registerConfigDataRoute } from './routes/enterprise_search/config_data'; +import { registerTelemetryRoute } from './routes/enterprise_search/telemetry'; +import { registerWorkplaceSearchRoutes } from './routes/workplace_search'; import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; -import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; -import { registerAppSearchRoutes } from './routes/app_search'; - +import { enterpriseSearchTelemetryType } from './saved_objects/enterprise_search/telemetry'; import { workplaceSearchTelemetryType } from './saved_objects/workplace_search/telemetry'; -import { registerTelemetryUsageCollector as registerWSTelemetryUsageCollector } from './collectors/workplace_search/telemetry'; -import { registerWorkplaceSearchRoutes } from './routes/workplace_search'; + +import { ConfigType } from './'; interface PluginsSetup { usageCollection?: UsageCollectionSetup; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index 49ff0353bef03..0070680985a34 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -7,8 +7,8 @@ import { schema } from '@kbn/config-schema'; -import { RouteDependencies } from '../../plugin'; import { ENGINES_PAGE_SIZE } from '../../../common/constants'; +import { RouteDependencies } from '../../plugin'; interface EnginesResponse { results: object[]; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 233e728a3010a..92fdcb689db1d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -7,12 +7,12 @@ import { RouteDependencies } from '../../plugin'; -import { registerEnginesRoutes } from './engines'; -import { registerCredentialsRoutes } from './credentials'; -import { registerSettingsRoutes } from './settings'; import { registerAnalyticsRoutes } from './analytics'; +import { registerCredentialsRoutes } from './credentials'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; +import { registerEnginesRoutes } from './engines'; import { registerSearchSettingsRoutes } from './search_settings'; +import { registerSettingsRoutes } from './settings'; export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerEnginesRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts index f9c65eedbb13a..e2cbd409bd396 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { RouteDependencies } from '../../plugin'; import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; +import { RouteDependencies } from '../../plugin'; export function registerConfigDataRoute({ router, config, log }: RouteDependencies) { router.get( diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index 08c398ba3eb0d..62f68748fcea1 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; import { MockRouter, mockLogger, mockDependencies } from '../../__mocks__'; +import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; + jest.mock('../../collectors/lib/telemetry', () => ({ incrementUICounter: jest.fn(), })); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts index c8750bdff5d38..90afba414c044 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts @@ -7,12 +7,13 @@ import { schema } from '@kbn/config-schema'; -import { RouteDependencies } from '../../plugin'; -import { incrementUICounter } from '../../collectors/lib/telemetry'; - -import { ES_TELEMETRY_NAME } from '../../collectors/enterprise_search/telemetry'; import { AS_TELEMETRY_NAME } from '../../collectors/app_search/telemetry'; +import { ES_TELEMETRY_NAME } from '../../collectors/enterprise_search/telemetry'; +import { incrementUICounter } from '../../collectors/lib/telemetry'; import { WS_TELEMETRY_NAME } from '../../collectors/workplace_search/telemetry'; + +import { RouteDependencies } from '../../plugin'; + const productToTelemetryMap = { enterprise_search: ES_TELEMETRY_NAME, app_search: AS_TELEMETRY_NAME, diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts index c4819c3579adc..cc6226e340653 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts @@ -7,11 +7,11 @@ import { RouteDependencies } from '../../plugin'; -import { registerOverviewRoute } from './overview'; import { registerGroupsRoutes } from './groups'; -import { registerSourcesRoutes } from './sources'; -import { registerSettingsRoutes } from './settings'; +import { registerOverviewRoute } from './overview'; import { registerSecurityRoutes } from './security'; +import { registerSettingsRoutes } from './settings'; +import { registerSourcesRoutes } from './sources'; export const registerWorkplaceSearchRoutes = (dependencies: RouteDependencies) => { registerOverviewRoute(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts index 29b1ce8182f52..ab873b6678885 100644 --- a/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts @@ -8,6 +8,7 @@ /* istanbul ignore file */ import { SavedObjectsType } from 'src/core/server'; + import { AS_TELEMETRY_NAME } from '../../collectors/app_search/telemetry'; export const appSearchTelemetryType: SavedObjectsType = { diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts index 07659299ef87f..e2edff1b6a213 100644 --- a/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts @@ -8,6 +8,7 @@ /* istanbul ignore file */ import { SavedObjectsType } from 'src/core/server'; + import { ES_TELEMETRY_NAME } from '../../collectors/enterprise_search/telemetry'; export const enterpriseSearchTelemetryType: SavedObjectsType = { diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/telemetry.ts index a466d69cf8343..af4d5908dec67 100644 --- a/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/telemetry.ts @@ -8,6 +8,7 @@ /* istanbul ignore file */ import { SavedObjectsType } from 'src/core/server'; + import { WS_TELEMETRY_NAME } from '../../collectors/workplace_search/telemetry'; export const workplaceSearchTelemetryType: SavedObjectsType = { From 08c08e88d93e525d7c00016830e35fe2080ccd72 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Feb 2021 10:08:46 -0800 Subject: [PATCH 066/100] [CI] Combines Jest unit tests (#89948) Signed-off-by: Tyler Smalley --- .ci/es-snapshots/Jenkinsfile_verify_es | 1 - .ci/jobs.yml | 1 - jest.config.integration.js | 5 +++- jest.config.js | 10 +++++++- jest.config.oss.js | 19 --------------- packages/kbn-test/jest-preset.js | 4 +++- .../shell_scripts/extract_archives.sh | 2 +- test/scripts/jenkins_unit.sh | 17 +++----------- test/scripts/jenkins_xpack.sh | 23 ------------------- test/scripts/test/jest_unit.sh | 4 +++- test/scripts/test/xpack_jest_unit.sh | 6 ----- vars/kibanaCoverage.groovy | 7 ------ vars/kibanaPipeline.groovy | 22 ++++++++---------- vars/workers.groovy | 4 ++-- x-pack/jest.config.js | 12 ---------- 15 files changed, 34 insertions(+), 103 deletions(-) delete mode 100644 jest.config.oss.js delete mode 100755 test/scripts/jenkins_xpack.sh delete mode 100755 test/scripts/test/xpack_jest_unit.sh delete mode 100644 x-pack/jest.config.js diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index 11a39faa9aed0..b40cd91a45c57 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -29,7 +29,6 @@ kibanaPipeline(timeoutMinutes: 150) { withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { parallel([ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), diff --git a/.ci/jobs.yml b/.ci/jobs.yml index b05e834f5a459..6aa93d4a1056a 100644 --- a/.ci/jobs.yml +++ b/.ci/jobs.yml @@ -2,7 +2,6 @@ JOB: - kibana-intake - - x-pack-intake - kibana-firefoxSmoke - kibana-ciGroup1 - kibana-ciGroup2 diff --git a/jest.config.integration.js b/jest.config.integration.js index df9fa9029aaa3..50767932a52d7 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -17,6 +17,7 @@ module.exports = { testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), + setupFilesAfterEnv: ['/packages/kbn-test/target/jest/setup/after_env.integration.js'], reporters: [ 'default', [ @@ -24,5 +25,7 @@ module.exports = { { reportName: 'Jest Integration Tests' }, ], ], - setupFilesAfterEnv: ['/packages/kbn-test/target/jest/setup/after_env.integration.js'], + coverageReporters: !!process.env.CI + ? [['json', { file: 'jest-integration.json' }]] + : ['html', 'text'], }; diff --git a/jest.config.js b/jest.config.js index 89f66b5ee462f..03dc832ba170c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,14 @@ */ module.exports = { + preset: '@kbn/test', rootDir: '.', - projects: [...require('./jest.config.oss').projects, ...require('./x-pack/jest.config').projects], + projects: [ + '/packages/*/jest.config.js', + '/src/*/jest.config.js', + '/src/legacy/*/jest.config.js', + '/src/plugins/*/jest.config.js', + '/test/*/jest.config.js', + '/x-pack/plugins/*/jest.config.js', + ], }; diff --git a/jest.config.oss.js b/jest.config.oss.js deleted file mode 100644 index fcd704382f39d..0000000000000 --- a/jest.config.oss.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '.', - projects: [ - '/packages/*/jest.config.js', - '/src/*/jest.config.js', - '/src/legacy/*/jest.config.js', - '/src/plugins/*/jest.config.js', - '/test/*/jest.config.js', - ], -}; diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 9ed11c4fe5fdd..717be8f413b48 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -19,7 +19,9 @@ module.exports = { coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts'], // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], + coverageReporters: !!process.env.CODE_COVERAGE + ? [['json', { file: 'jest.json' }]] + : ['html', 'text'], // An array of file extensions your modules use moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], diff --git a/src/dev/code_coverage/shell_scripts/extract_archives.sh b/src/dev/code_coverage/shell_scripts/extract_archives.sh index 376467f9f2e55..14b35f8786d02 100644 --- a/src/dev/code_coverage/shell_scripts/extract_archives.sh +++ b/src/dev/code_coverage/shell_scripts/extract_archives.sh @@ -6,7 +6,7 @@ EXTRACT_DIR=/tmp/extracted_coverage mkdir -p $EXTRACT_DIR echo "### Extracting downloaded artifacts" -for x in kibana-intake x-pack-intake kibana-oss-tests kibana-xpack-tests; do +for x in kibana-intake kibana-oss-tests kibana-xpack-tests; do tar -xzf $DOWNLOAD_DIR/coverage/${x}/kibana-coverage.tar.gz -C $EXTRACT_DIR || echo "### Error 'tarring': ${x}" done diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 6e28f9c3ef56a..9e387f97a016e 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -2,12 +2,6 @@ source test/scripts/jenkins_test_setup.sh -rename_coverage_file() { - test -f target/kibana-coverage/jest/coverage-final.json \ - && mv target/kibana-coverage/jest/coverage-final.json \ - target/kibana-coverage/jest/$1-coverage-final.json -} - if [[ -z "$CODE_COVERAGE" ]] ; then # Lint ./test/scripts/lint/eslint.sh @@ -34,13 +28,8 @@ if [[ -z "$CODE_COVERAGE" ]] ; then ./test/scripts/checks/test_hardening.sh else echo " -> Running jest tests with coverage" - node scripts/jest --ci --verbose --coverage --config jest.config.oss.js || true; - rename_coverage_file "oss" - echo "" - echo "" + node scripts/jest --ci --verbose --maxWorkers=6 --coverage || true; + echo " -> Running jest integration tests with coverage" - node --max-old-space-size=8192 scripts/jest_integration --ci --verbose --coverage || true; - rename_coverage_file "oss-integration" - echo "" - echo "" + node scripts/jest_integration --ci --verbose --coverage || true; fi diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh deleted file mode 100755 index 66fb5ae5370bc..0000000000000 --- a/test/scripts/jenkins_xpack.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -source test/scripts/jenkins_test_setup.sh - -if [[ -z "$CODE_COVERAGE" ]] ; then - echo " -> Running jest tests" - - ./test/scripts/test/xpack_jest_unit.sh -else - echo " -> Build runtime for canvas" - # build runtime for canvas - echo "NODE_ENV=$NODE_ENV" - node ./x-pack/plugins/canvas/scripts/shareable_runtime - echo " -> Running jest tests with coverage" - cd x-pack - node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=5 --coverage --config jest.config.js || true; - # 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 \ - ../target/kibana-coverage/jest/xpack-coverage-final.json - echo "" - echo "" -fi diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index 88c0fe528b88c..1442a0f728727 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -2,5 +2,7 @@ source src/dev/ci_setup/setup_env.sh +export NODE_OPTIONS="--max-old-space-size=2048" + checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --config jest.config.oss.js --ci --verbose --maxWorkers=5 + node scripts/jest --ci --verbose --maxWorkers=8 diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh deleted file mode 100755 index 33b1c8a2b5183..0000000000000 --- a/test/scripts/test/xpack_jest_unit.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -checks-reporter-with-killswitch "X-Pack Jest" \ - node scripts/jest x-pack --ci --verbose --maxWorkers=5 diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 609d8f78aeb96..e393f3a5d2150 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -197,13 +197,6 @@ def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, teamAssignmen def runTests() { parallel([ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() - } - }, 'kibana-oss-agent' : workers.functional( 'kibana-oss-tests', { kibanaPipeline.buildOss() }, diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 17349f6b566dc..5efcea3edb9bb 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -186,20 +186,21 @@ def uploadCoverageArtifacts(prefix, pattern) { def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ + 'target/junit/**/*', 'target/kibana-*', - 'target/test-metrics/*', + 'target/kibana-coverage/**/*', 'target/kibana-security-solution/**/*.png', - 'target/junit/**/*', + 'target/test-metrics/*', 'target/test-suites-ci-plan.json', - 'test/**/screenshots/session/*.png', - 'test/**/screenshots/failure/*.png', 'test/**/screenshots/diff/*.png', + 'test/**/screenshots/failure/*.png', + 'test/**/screenshots/session/*.png', 'test/functional/failure_debug/html/*.html', - 'x-pack/test/**/screenshots/session/*.png', - 'x-pack/test/**/screenshots/failure/*.png', 'x-pack/test/**/screenshots/diff/*.png', - 'x-pack/test/functional/failure_debug/html/*.html', + 'x-pack/test/**/screenshots/failure/*.png', + 'x-pack/test/**/screenshots/session/*.png', 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', + 'x-pack/test/functional/failure_debug/html/*.html', ] withEnv([ @@ -462,15 +463,10 @@ def allCiTasks() { } }, jest: { - workers.ci(name: 'jest', size: 'c2-8', ramDisk: true) { + workers.ci(name: 'jest', size: 'n2-standard-16', ramDisk: false) { scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh')() } }, - xpackJest: { - workers.ci(name: 'xpack-jest', size: 'c2-8', ramDisk: true) { - scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh')() - } - }, ]) } diff --git a/vars/workers.groovy b/vars/workers.groovy index e1684f7aadb43..5d3328bc8a3c4 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -19,8 +19,8 @@ def label(size) { return 'docker && tests-xl-highmem' case 'xxl': return 'docker && tests-xxl && gobld/machineType:custom-64-270336' - case 'c2-8': - return 'docker && linux && immutable && gobld/machineType:c2-standard-8' + case 'n2-standard-16': + return 'docker && linux && immutable && gobld/machineType:n2-standard-16' } error "unknown size '${size}'" diff --git a/x-pack/jest.config.js b/x-pack/jest.config.js deleted file mode 100644 index 231004359632b..0000000000000 --- a/x-pack/jest.config.js +++ /dev/null @@ -1,12 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '..', - projects: ['/x-pack/plugins/*/jest.config.js'], -}; From 1f5d52ea2e3dd1aeb0292fdbe3a2af378fba923d Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Tue, 9 Feb 2021 13:28:46 -0500 Subject: [PATCH 067/100] [Metrics UI] Add ability for user to set anomaly threshold (#90313) * add anomaly threshold configuration to settings * hide panel if not ml capable * send threshold value to query * update license * update api integration to expect anomalyThreshold to exist * add some tests to the anomaly queries * change type to number * api validate in source update and config for anamolyThreshold and tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../results/metrics_hosts_anomalies.ts | 1 + .../infra_ml/results/metrics_k8s_anomalies.ts | 1 + .../infra/common/http_api/source_api.ts | 4 + .../components/expression_chart.test.tsx | 1 + .../indices_configuration_form_state.ts | 21 ++++- .../source_configuration/input_fields.tsx | 44 +++++++++- .../ml_configuration_panel.tsx | 85 +++++++++++++++++++ .../source_configuration_form_state.tsx | 3 + .../source_configuration_settings.tsx | 17 +++- .../components/timeline/timeline.tsx | 3 +- .../hooks/use_metrics_hosts_anomalies.ts | 8 +- .../hooks/use_metrics_k8s_anomalies.ts | 17 +++- .../public/utils/fixtures/metrics_explorer.ts | 1 + .../lib/infra_ml/metrics_hosts_anomalies.ts | 12 ++- .../lib/infra_ml/metrics_k8s_anomalies.ts | 12 ++- .../queries/metrics_host_anomalies.test.ts | 63 ++++++++++++++ .../queries/metrics_hosts_anomalies.ts | 24 ++++-- .../queries/metrics_k8s_anomalies.test.ts | 62 ++++++++++++++ .../infra_ml/queries/metrics_k8s_anomalies.ts | 24 ++++-- .../infra/server/lib/sources/defaults.ts | 1 + .../infra/server/lib/sources/errors.ts | 9 +- ..._new_indexing_strategy_index_names.test.ts | 1 + .../infra/server/lib/sources/sources.ts | 10 ++- .../results/metrics_hosts_anomalies.ts | 2 + .../infra_ml/results/metrics_k8s_anomalies.ts | 2 + .../infra/server/routes/source/index.ts | 10 +++ .../log_entries_search_strategy.test.ts | 1 + .../log_entry_search_strategy.test.ts | 1 + .../apis/metrics_ui/sources.ts | 29 ++++++- 29 files changed, 441 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/source_configuration/ml_configuration_panel.tsx create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts index 27574d01be898..0b70b65b7069e 100644 --- a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts @@ -62,6 +62,7 @@ export const getMetricsHostsAnomaliesRequestPayloadRT = rt.type({ rt.type({ // the ID of the source configuration sourceId: rt.string, + anomalyThreshold: rt.number, // the time range to fetch the log entry anomalies from timeRange: timeRangeRT, }), diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts index 3c2615a447b07..3ee6189dcbf9a 100644 --- a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts @@ -62,6 +62,7 @@ export const getMetricsK8sAnomaliesRequestPayloadRT = rt.type({ rt.type({ // the ID of the source configuration sourceId: rt.string, + anomalyThreshold: rt.number, // the time range to fetch the log entry anomalies from timeRange: timeRangeRT, }), diff --git a/x-pack/plugins/infra/common/http_api/source_api.ts b/x-pack/plugins/infra/common/http_api/source_api.ts index 257383be859aa..f14151531ba35 100644 --- a/x-pack/plugins/infra/common/http_api/source_api.ts +++ b/x-pack/plugins/infra/common/http_api/source_api.ts @@ -90,6 +90,7 @@ export const SavedSourceConfigurationRuntimeType = rt.partial({ metricsExplorerDefaultView: rt.string, fields: SavedSourceConfigurationFieldsRuntimeType, logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), + anomalyThreshold: rt.number, }); export interface InfraSavedSourceConfiguration @@ -107,6 +108,7 @@ export const pickSavedSourceConfiguration = ( inventoryDefaultView, metricsExplorerDefaultView, logColumns, + anomalyThreshold, } = value; const { container, host, pod, tiebreaker, timestamp } = fields; @@ -119,6 +121,7 @@ export const pickSavedSourceConfiguration = ( metricsExplorerDefaultView, fields: { container, host, pod, tiebreaker, timestamp }, logColumns, + anomalyThreshold, }; }; @@ -140,6 +143,7 @@ export const StaticSourceConfigurationRuntimeType = rt.partial({ metricsExplorerDefaultView: rt.string, fields: StaticSourceConfigurationFieldsRuntimeType, logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), + anomalyThreshold: rt.number, }); export interface InfraStaticSourceConfiguration diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index 939834fa7c4a8..7e4209e4253d7 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -64,6 +64,7 @@ describe('ExpressionChart', () => { pod: 'kubernetes.pod.uid', tiebreaker: '_doc', }, + anomalyThreshold: 20, }, }; diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts index 794048a8b3a3a..b4dede79d11f2 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts +++ b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts @@ -7,7 +7,11 @@ import { ReactNode, useCallback, useMemo, useState } from 'react'; -import { createInputFieldProps, validateInputFieldNotEmpty } from './input_fields'; +import { + createInputFieldProps, + createInputRangeFieldProps, + validateInputFieldNotEmpty, +} from './input_fields'; interface FormState { name: string; @@ -20,6 +24,7 @@ interface FormState { podField: string; tiebreakerField: string; timestampField: string; + anomalyThreshold: number; } type FormStateChanges = Partial; @@ -124,6 +129,17 @@ export const useIndicesConfigurationFormState = ({ }), [formState.timestampField] ); + const anomalyThresholdFieldProps = useMemo( + () => + createInputRangeFieldProps({ + errors: validateInputFieldNotEmpty(formState.anomalyThreshold), + name: 'anomalyThreshold', + onChange: (anomalyThreshold) => + setFormStateChanges((changes) => ({ ...changes, anomalyThreshold })), + value: formState.anomalyThreshold, + }), + [formState.anomalyThreshold] + ); const fieldProps = useMemo( () => ({ @@ -135,6 +151,7 @@ export const useIndicesConfigurationFormState = ({ podField: podFieldFieldProps, tiebreakerField: tiebreakerFieldFieldProps, timestampField: timestampFieldFieldProps, + anomalyThreshold: anomalyThresholdFieldProps, }), [ nameFieldProps, @@ -145,6 +162,7 @@ export const useIndicesConfigurationFormState = ({ podFieldFieldProps, tiebreakerFieldFieldProps, timestampFieldFieldProps, + anomalyThresholdFieldProps, ] ); @@ -183,4 +201,5 @@ const defaultFormState: FormState = { podField: '', tiebreakerField: '', timestampField: '', + anomalyThreshold: 0, }; diff --git a/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx b/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx index b8832d27a0a4d..a7a842417ebc2 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { ReactText } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -43,7 +43,47 @@ export const createInputFieldProps = < value, }); -export const validateInputFieldNotEmpty = (value: string) => +export interface InputRangeFieldProps< + Value extends ReactText = ReactText, + FieldElement extends HTMLInputElement = HTMLInputElement, + ButtonElement extends HTMLButtonElement = HTMLButtonElement +> { + error: React.ReactNode[]; + isInvalid: boolean; + name: string; + onChange?: ( + evt: React.ChangeEvent | React.MouseEvent, + isValid: boolean + ) => void; + value: Value; +} + +export const createInputRangeFieldProps = < + Value extends ReactText = ReactText, + FieldElement extends HTMLInputElement = HTMLInputElement, + ButtonElement extends HTMLButtonElement = HTMLButtonElement +>({ + errors, + name, + onChange, + value, +}: { + errors: FieldErrorMessage[]; + name: string; + onChange: (newValue: number, isValid: boolean) => void; + value: Value; +}): InputRangeFieldProps => ({ + error: errors, + isInvalid: errors.length > 0, + name, + onChange: ( + evt: React.ChangeEvent | React.MouseEvent, + isValid: boolean + ) => onChange(+evt.currentTarget.value, isValid), + value, +}); + +export const validateInputFieldNotEmpty = (value: React.ReactText) => value === '' ? [ { + return ( + + +

+ +

+
+ + + + + } + description={ + + } + > + + } + > + + + +
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx index 3f947bdb40677..c80235137eea6 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx @@ -27,6 +27,7 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi podField: configuration.fields.pod, tiebreakerField: configuration.fields.tiebreaker, timestampField: configuration.fields.timestamp, + anomalyThreshold: configuration.anomalyThreshold, } : undefined, [configuration] @@ -79,6 +80,7 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi timestamp: indicesConfigurationFormState.formState.timestampField, }, logColumns: logColumnsConfigurationFormState.formState.logColumns, + anomalyThreshold: indicesConfigurationFormState.formState.anomalyThreshold, }), [indicesConfigurationFormState.formState, logColumnsConfigurationFormState.formState] ); @@ -97,6 +99,7 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi timestamp: indicesConfigurationFormState.formStateChanges.timestampField, }, logColumns: logColumnsConfigurationFormState.formStateChanges.logColumns, + anomalyThreshold: indicesConfigurationFormState.formStateChanges.anomalyThreshold, }), [ indicesConfigurationFormState.formStateChanges, diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index bdf4584bc6287..4b609a881bd18 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -26,6 +26,8 @@ import { NameConfigurationPanel } from './name_configuration_panel'; import { useSourceConfigurationFormState } from './source_configuration_form_state'; import { SourceLoadingPage } from '../source_loading_page'; import { Prompt } from '../../utils/navigation_warning_prompt'; +import { MLConfigurationPanel } from './ml_configuration_panel'; +import { useInfraMLCapabilitiesContext } from '../../containers/ml/infra_ml_capabilities'; interface SourceConfigurationSettingsProps { shouldAllowEdit: boolean; @@ -52,7 +54,6 @@ export const SourceConfigurationSettings = ({ formState, formStateChanges, } = useSourceConfigurationFormState(source && source.configuration); - const persistUpdates = useCallback(async () => { if (sourceExists) { await updateSourceConfiguration(formStateChanges); @@ -74,6 +75,8 @@ export const SourceConfigurationSettings = ({ source, ]); + const { hasInfraMLCapabilites } = useInfraMLCapabilitiesContext(); + if ((isLoading || isUninitialized) && !source) { return ; } @@ -125,6 +128,18 @@ export const SourceConfigurationSettings = ({ /> + {hasInfraMLCapabilites && ( + <> + + + + + + )} {errors.length > 0 ? ( <> diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx index a6a296f7d5725..0248241d616dc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -51,7 +51,7 @@ interface Props { } export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible }) => { - const { sourceId } = useSourceContext(); + const { sourceId, source } = useSourceContext(); const { metric, nodeType, accountId, region } = useWaffleOptionsContext(); const { currentTime, jumpToTime, stopAutoReload } = useWaffleTimeContext(); const { filterQueryAsJson } = useWaffleFiltersContext(); @@ -70,6 +70,7 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible const anomalyParams = { sourceId: 'default', + anomalyThreshold: source?.configuration.anomalyThreshold || 0, startTime, endTime, defaultSortOptions: { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts index c3732fb22cb63..25afd05633fa5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts @@ -138,6 +138,7 @@ export const useMetricsHostsAnomaliesResults = ({ endTime, startTime, sourceId, + anomalyThreshold, defaultSortOptions, defaultPaginationOptions, onGetMetricsHostsAnomaliesDatasetsError, @@ -146,6 +147,7 @@ export const useMetricsHostsAnomaliesResults = ({ endTime: number; startTime: number; sourceId: string; + anomalyThreshold: number; defaultSortOptions: Sort; defaultPaginationOptions: Pick; onGetMetricsHostsAnomaliesDatasetsError?: (error: Error) => void; @@ -182,6 +184,7 @@ export const useMetricsHostsAnomaliesResults = ({ return await callGetMetricHostsAnomaliesAPI( { sourceId, + anomalyThreshold, startTime: queryStartTime, endTime: queryEndTime, metric, @@ -215,6 +218,7 @@ export const useMetricsHostsAnomaliesResults = ({ }, [ sourceId, + anomalyThreshold, dispatch, reducerState.timeRange, reducerState.sortOptions, @@ -296,6 +300,7 @@ export const useMetricsHostsAnomaliesResults = ({ interface RequestArgs { sourceId: string; + anomalyThreshold: number; startTime: number; endTime: number; metric: Metric; @@ -307,13 +312,14 @@ export const callGetMetricHostsAnomaliesAPI = async ( requestArgs: RequestArgs, fetch: HttpHandler ) => { - const { sourceId, startTime, endTime, metric, sort, pagination } = requestArgs; + const { sourceId, anomalyThreshold, startTime, endTime, metric, sort, pagination } = requestArgs; const response = await fetch(INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH, { method: 'POST', body: JSON.stringify( getMetricsHostsAnomaliesRequestPayloadRT.encode({ data: { sourceId, + anomalyThreshold, timeRange: { startTime, endTime, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts index 2a8beeaa814fc..c135a2c5e6661 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts @@ -138,6 +138,7 @@ export const useMetricsK8sAnomaliesResults = ({ endTime, startTime, sourceId, + anomalyThreshold, defaultSortOptions, defaultPaginationOptions, onGetMetricsHostsAnomaliesDatasetsError, @@ -146,6 +147,7 @@ export const useMetricsK8sAnomaliesResults = ({ endTime: number; startTime: number; sourceId: string; + anomalyThreshold: number; defaultSortOptions: Sort; defaultPaginationOptions: Pick; onGetMetricsHostsAnomaliesDatasetsError?: (error: Error) => void; @@ -183,6 +185,7 @@ export const useMetricsK8sAnomaliesResults = ({ return await callGetMetricsK8sAnomaliesAPI( { sourceId, + anomalyThreshold, startTime: queryStartTime, endTime: queryEndTime, metric, @@ -217,6 +220,7 @@ export const useMetricsK8sAnomaliesResults = ({ }, [ sourceId, + anomalyThreshold, dispatch, reducerState.timeRange, reducerState.sortOptions, @@ -298,6 +302,7 @@ export const useMetricsK8sAnomaliesResults = ({ interface RequestArgs { sourceId: string; + anomalyThreshold: number; startTime: number; endTime: number; metric: Metric; @@ -310,13 +315,23 @@ export const callGetMetricsK8sAnomaliesAPI = async ( requestArgs: RequestArgs, fetch: HttpHandler ) => { - const { sourceId, startTime, endTime, metric, sort, pagination, datasets } = requestArgs; + const { + sourceId, + anomalyThreshold, + startTime, + endTime, + metric, + sort, + pagination, + datasets, + } = requestArgs; const response = await fetch(INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH, { method: 'POST', body: JSON.stringify( getMetricsK8sAnomaliesRequestPayloadRT.encode({ data: { sourceId, + anomalyThreshold, timeRange: { startTime, endTime, diff --git a/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts b/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts index 6d8f9ae476044..27648b6d7b193 100644 --- a/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts +++ b/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts @@ -40,6 +40,7 @@ export const source = { message: ['message'], tiebreaker: '@timestamp', }, + anomalyThreshold: 20, }; export const chartOptions: MetricsExplorerChartOptions = { diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts index 072f07dfaffdb..7873fd8e43a7b 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts @@ -76,6 +76,7 @@ async function getCompatibleAnomaliesJobIds( export async function getMetricsHostsAnomalies( context: InfraPluginRequestHandlerContext & { infra: Required }, sourceId: string, + anomalyThreshold: number, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, @@ -108,6 +109,7 @@ export async function getMetricsHostsAnomalies( timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricsHostsAnomalies( context.infra.mlSystem, + anomalyThreshold, jobIds, startTime, endTime, @@ -162,6 +164,7 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricsHostsAnomalies( mlSystem: MlSystem, + anomalyThreshold: number, jobIds: string[], startTime: number, endTime: number, @@ -178,7 +181,14 @@ async function fetchMetricsHostsAnomalies( const results = decodeOrThrow(metricsHostsAnomaliesResponseRT)( await mlSystem.mlAnomalySearch( - createMetricsHostsAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination), + createMetricsHostsAnomaliesQuery({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination: expandedPagination, + }), jobIds ) ); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts index 44837d88ddb43..0c87b2f0f8b53 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts @@ -76,6 +76,7 @@ async function getCompatibleAnomaliesJobIds( export async function getMetricK8sAnomalies( context: InfraPluginRequestHandlerContext & { infra: Required }, sourceId: string, + anomalyThreshold: number, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, @@ -107,6 +108,7 @@ export async function getMetricK8sAnomalies( timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricK8sAnomalies( context.infra.mlSystem, + anomalyThreshold, jobIds, startTime, endTime, @@ -158,6 +160,7 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricK8sAnomalies( mlSystem: MlSystem, + anomalyThreshold: number, jobIds: string[], startTime: number, endTime: number, @@ -174,7 +177,14 @@ async function fetchMetricK8sAnomalies( const results = decodeOrThrow(metricsK8sAnomaliesResponseRT)( await mlSystem.mlAnomalySearch( - createMetricsK8sAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination), + createMetricsK8sAnomaliesQuery({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination: expandedPagination, + }), jobIds ) ); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts new file mode 100644 index 0000000000000..4c3e0ca8bc26f --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createMetricsHostsAnomaliesQuery } from './metrics_hosts_anomalies'; +import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; + +describe('createMetricsHostAnomaliesQuery', () => { + const jobIds = ['kibana-metrics-ui-default-default-hosts_memory_usage']; + const anomalyThreshold = 30; + const startTime = 1612454527112; + const endTime = 1612541227112; + const sort: Sort = { field: 'anomalyScore', direction: 'desc' }; + const pagination: Pagination = { pageSize: 101 }; + + test('returns the correct query', () => { + expect( + createMetricsHostsAnomaliesQuery({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination, + }) + ).toMatchObject({ + allowNoIndices: true, + ignoreUnavailable: true, + trackScores: false, + trackTotalHits: false, + body: { + query: { + bool: { + filter: [ + { terms: { job_id: ['kibana-metrics-ui-default-default-hosts_memory_usage'] } }, + { range: { record_score: { gte: 30 } } }, + { range: { timestamp: { gte: 1612454527112, lte: 1612541227112 } } }, + { terms: { result_type: ['record'] } }, + ], + }, + }, + sort: [{ record_score: 'desc' }, { _doc: 'desc' }], + size: 101, + _source: [ + 'job_id', + 'record_score', + 'typical', + 'actual', + 'partition_field_value', + 'timestamp', + 'bucket_span', + 'by_field_value', + 'host.name', + 'influencers.influencer_field_name', + 'influencers.influencer_field_values', + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts index 07b25931d838e..45587cd258e5d 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts @@ -25,19 +25,27 @@ const sortToMlFieldMap = { startTime: 'timestamp', }; -export const createMetricsHostsAnomaliesQuery = ( - jobIds: string[], - startTime: number, - endTime: number, - sort: Sort, - pagination: Pagination -) => { +export const createMetricsHostsAnomaliesQuery = ({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination, +}: { + jobIds: string[]; + anomalyThreshold: number; + startTime: number; + endTime: number; + sort: Sort; + pagination: Pagination; +}) => { const { field } = sort; const { pageSize } = pagination; const filters = [ ...createJobIdsFilters(jobIds), - ...createAnomalyScoreFilter(50), + ...createAnomalyScoreFilter(anomalyThreshold), ...createTimeRangeFilters(startTime, endTime), ...createResultTypeFilters(['record']), ]; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts new file mode 100644 index 0000000000000..81dcb390dff56 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createMetricsK8sAnomaliesQuery } from './metrics_k8s_anomalies'; +import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; + +describe('createMetricsK8sAnomaliesQuery', () => { + const jobIds = ['kibana-metrics-ui-default-default-k8s_memory_usage']; + const anomalyThreshold = 30; + const startTime = 1612454527112; + const endTime = 1612541227112; + const sort: Sort = { field: 'anomalyScore', direction: 'desc' }; + const pagination: Pagination = { pageSize: 101 }; + + test('returns the correct query', () => { + expect( + createMetricsK8sAnomaliesQuery({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination, + }) + ).toMatchObject({ + allowNoIndices: true, + ignoreUnavailable: true, + trackScores: false, + trackTotalHits: false, + body: { + query: { + bool: { + filter: [ + { terms: { job_id: ['kibana-metrics-ui-default-default-k8s_memory_usage'] } }, + { range: { record_score: { gte: 30 } } }, + { range: { timestamp: { gte: 1612454527112, lte: 1612541227112 } } }, + { terms: { result_type: ['record'] } }, + ], + }, + }, + sort: [{ record_score: 'desc' }, { _doc: 'desc' }], + size: 101, + _source: [ + 'job_id', + 'record_score', + 'typical', + 'actual', + 'partition_field_value', + 'timestamp', + 'bucket_span', + 'by_field_value', + 'influencers.influencer_field_name', + 'influencers.influencer_field_values', + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts index 8a6e9396fb098..56a4b99e7236c 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts @@ -25,19 +25,27 @@ const sortToMlFieldMap = { startTime: 'timestamp', }; -export const createMetricsK8sAnomaliesQuery = ( - jobIds: string[], - startTime: number, - endTime: number, - sort: Sort, - pagination: Pagination -) => { +export const createMetricsK8sAnomaliesQuery = ({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination, +}: { + jobIds: string[]; + anomalyThreshold: number; + startTime: number; + endTime: number; + sort: Sort; + pagination: Pagination; +}) => { const { field } = sort; const { pageSize } = pagination; const filters = [ ...createJobIdsFilters(jobIds), - ...createAnomalyScoreFilter(50), + ...createAnomalyScoreFilter(anomalyThreshold), ...createTimeRangeFilters(startTime, endTime), ...createResultTypeFilters(['record']), ]; diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index ce7c4410baca9..1b924619a905c 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -45,4 +45,5 @@ export const defaultSourceConfiguration: InfraSourceConfiguration = { }, }, ], + anomalyThreshold: 50, }; diff --git a/x-pack/plugins/infra/server/lib/sources/errors.ts b/x-pack/plugins/infra/server/lib/sources/errors.ts index fb0dc3b031511..082dfc611cc5b 100644 --- a/x-pack/plugins/infra/server/lib/sources/errors.ts +++ b/x-pack/plugins/infra/server/lib/sources/errors.ts @@ -4,10 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +/* eslint-disable max-classes-per-file */ export class NotFoundError extends Error { constructor(message?: string) { super(message); Object.setPrototypeOf(this, new.target.prototype); } } + +export class AnomalyThresholdRangeError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts index 4cea6cbe32cfb..21b7643ca6a7f 100644 --- a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts @@ -126,6 +126,7 @@ const createTestSourceConfiguration = (logAlias: string, metricAlias: string) => ], logAlias, metricAlias, + anomalyThreshold: 20, }, id: 'TEST_ID', type: infraSourceConfigurationSavedObjectName, diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index aad877a077acf..fe005b04978da 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -10,9 +10,10 @@ import { failure } from 'io-ts/lib/PathReporter'; import { identity, constant } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; +import { inRange } from 'lodash'; import { SavedObjectsClientContract } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; -import { NotFoundError } from './errors'; +import { AnomalyThresholdRangeError, NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectName } from './saved_object_type'; import { InfraSavedSourceConfiguration, @@ -104,6 +105,9 @@ export class InfraSources { source: InfraSavedSourceConfiguration ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); + const { anomalyThreshold } = source; + if (anomalyThreshold && !inRange(anomalyThreshold, 0, 101)) + throw new AnomalyThresholdRangeError('anomalyThreshold must be 1-100'); const newSourceConfiguration = mergeSourceConfiguration( staticDefaultSourceConfiguration, @@ -140,6 +144,10 @@ export class InfraSources { sourceProperties: InfraSavedSourceConfiguration ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); + const { anomalyThreshold } = sourceProperties; + + if (anomalyThreshold && !inRange(anomalyThreshold, 0, 101)) + throw new AnomalyThresholdRangeError('anomalyThreshold must be 1-100'); const { configuration, version } = await this.getSourceConfiguration( savedObjectsClient, diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts index 215ebf3280c03..8ec0b83994e1a 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts @@ -34,6 +34,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { const { data: { sourceId, + anomalyThreshold, timeRange: { startTime, endTime }, sort: sortParam, pagination: paginationParam, @@ -54,6 +55,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { } = await getMetricsHostsAnomalies( requestContext, sourceId, + anomalyThreshold, startTime, endTime, metric, diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts index 906278be657d3..d41fa0ffafecc 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts @@ -33,6 +33,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => { const { data: { sourceId, + anomalyThreshold, timeRange: { startTime, endTime }, sort: sortParam, pagination: paginationParam, @@ -53,6 +54,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => { } = await getMetricK8sAnomalies( requestContext, sourceId, + anomalyThreshold, startTime, endTime, metric, diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts index f1132049bd03c..5c3827e56ce79 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -16,6 +16,7 @@ import { import { InfraBackendLibs } from '../../lib/infra_types'; import { hasData } from '../../lib/sources/has_data'; import { createSearchClient } from '../../lib/create_search_client'; +import { AnomalyThresholdRangeError } from '../../lib/sources/errors'; const typeToInfraIndexType = (value: string | undefined) => { switch (value) { @@ -137,6 +138,15 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { throw error; } + if (error instanceof AnomalyThresholdRangeError) { + return response.customError({ + statusCode: 400, + body: { + message: error.message, + }, + }); + } + return response.customError({ statusCode: error.statusCode ?? 500, body: { diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts index 16a45dc6489ee..bc4976a068f4d 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts @@ -279,6 +279,7 @@ const createSourceConfigurationMock = (): InfraSource => ({ timestamp: 'TIMESTAMP_FIELD', tiebreaker: 'TIEBREAKER_FIELD', }, + anomalyThreshold: 20, }, }); diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts index 6bcc61f2be4a6..7ac8b71c04b2a 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts @@ -216,6 +216,7 @@ const createSourceConfigurationMock = () => ({ timestamp: 'TIMESTAMP_FIELD', tiebreaker: 'TIEBREAKER_FIELD', }, + anomalyThreshold: 20, }, }); diff --git a/x-pack/test/api_integration/apis/metrics_ui/sources.ts b/x-pack/test/api_integration/apis/metrics_ui/sources.ts index 7fb631477cb76..a5bab8de92f38 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/sources.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/sources.ts @@ -17,11 +17,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); + const SOURCE_API_URL = '/api/metrics/source/default'; const patchRequest = async ( body: InfraSavedSourceConfiguration ): Promise => { const response = await supertest - .patch('/api/metrics/source/default') + .patch(SOURCE_API_URL) .set('kbn-xsrf', 'xxx') .send(body) .expect(200); @@ -73,6 +74,7 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration?.fields.timestamp).to.be('@timestamp'); expect(configuration?.fields.container).to.be('container.id'); expect(configuration?.logColumns).to.have.length(3); + expect(configuration?.anomalyThreshold).to.be(50); expect(status?.logIndicesExist).to.be(true); expect(status?.metricIndicesExist).to.be(true); }); @@ -173,6 +175,31 @@ export default function ({ getService }: FtrProviderContext) { expect(fieldColumn).to.have.property('id', 'ADDED_COLUMN_ID'); expect(fieldColumn).to.have.property('field', 'ADDED_COLUMN_FIELD'); }); + it('validates anomalyThreshold is between range 1-100', async () => { + // create config with bad request + await supertest + .patch(SOURCE_API_URL) + .set('kbn-xsrf', 'xxx') + .send({ name: 'NAME', anomalyThreshold: -20 }) + .expect(400); + // create config with good request + await supertest + .patch(SOURCE_API_URL) + .set('kbn-xsrf', 'xxx') + .send({ name: 'NAME', anomalyThreshold: 20 }) + .expect(200); + + await supertest + .patch(SOURCE_API_URL) + .set('kbn-xsrf', 'xxx') + .send({ anomalyThreshold: -2 }) + .expect(400); + await supertest + .patch(SOURCE_API_URL) + .set('kbn-xsrf', 'xxx') + .send({ anomalyThreshold: 101 }) + .expect(400); + }); }); }); } From 5f8de693b9183d056bd9666309e6817331f3f017 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Tue, 9 Feb 2021 14:07:53 -0500 Subject: [PATCH 068/100] [Alerting] Configurable number of hits for ES query alert (#90089) * Adding size parameter to ES query alert * Can't use const inside validation * Updating docs * Fixing functional test * License Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/alerting/alert-types.asciidoc | 3 +- .../alert-types-es-query-conditions.png | Bin 97147 -> 107595 bytes .../alert_types/es_query/expression.test.tsx | 3 + .../alert_types/es_query/expression.tsx | 21 ++- .../public/alert_types/es_query/types.ts | 1 + .../alert_types/es_query/validation.test.ts | 37 +++++ .../public/alert_types/es_query/validation.ts | 26 +++- .../es_query/action_context.test.ts | 2 + .../alert_types/es_query/alert_type.test.ts | 6 + .../server/alert_types/es_query/alert_type.ts | 14 +- .../es_query/alert_type_params.test.ts | 29 +++- .../alert_types/es_query/alert_type_params.ts | 3 + .../public/common/expression_items/index.ts | 1 + .../common/expression_items/value.test.tsx | 136 ++++++++++++++++++ .../public/common/expression_items/value.tsx | 102 +++++++++++++ .../builtin_alert_types/es_query/alert.ts | 6 + 16 files changed, 382 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 279739e95b522..016ecc3167298 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -130,12 +130,13 @@ image::images/alert-types-es-query-select.png[Choosing an ES query alert type] [float] ==== Defining the conditions -The ES query alert has 4 clauses that define the condition to detect. +The ES query alert has 5 clauses that define the condition to detect. [role="screenshot"] image::images/alert-types-es-query-conditions.png[Four clauses define the condition to detect] Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. +Size:: This clause specifies the number of documents to pass to the configured actions when the the threshold condition is met. ES query:: This clause specifies the ES DSL query to execute. The number of documents that match this query will be evaulated against the threshold condition. Aggregations are not supported at this time. Threshold:: This clause defines a threshold value and a comparison operator (`is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The number of documents that match the specified query is compared to this threshold. diff --git a/docs/user/alerting/images/alert-types-es-query-conditions.png b/docs/user/alerting/images/alert-types-es-query-conditions.png index ce2bd6a42a4b5c2256121bf513e751988c7cb3e3..3cbba5eb4950e20ce45bb19c1fc3ba5bb0704626 100644 GIT binary patch literal 107595 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%0A;SKFnF$eJnoDw*SC4vu*ARKg;B@sQx#R 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 H%jf?Ea*#}v literal 97147 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
  • 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 { index: ['test-index'], timeField: '@timestamp', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, thresholdComparator: '>', threshold: [0], timeWindowSize: 15, @@ -137,6 +138,7 @@ describe('EsQueryAlertTypeExpression', () => { const errors = { index: [], esQuery: [], + size: [], timeField: [], timeWindowSize: [], }; @@ -169,6 +171,7 @@ describe('EsQueryAlertTypeExpression', () => { test('should render EsQueryAlertTypeExpression with expected components', async () => { const wrapper = await setup(getAlertParams()); expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="sizeValueExpression"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="queryJsonEditor"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="testQuerySuccess"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="testQueryError"]').exists()).toBeFalsy(); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx index 27f8071564c55..37c64688ec49a 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx @@ -30,6 +30,7 @@ import { COMPARATORS, ThresholdExpression, ForLastExpression, + ValueExpression, AlertTypeParamsExpressionProps, } from '../../../../triggers_actions_ui/public'; import { validateExpression } from './validation'; @@ -45,6 +46,7 @@ const DEFAULT_VALUES = { "match_all" : {} } }`, + SIZE: 100, TIME_WINDOW_SIZE: 5, TIME_WINDOW_UNIT: 'm', THRESHOLD: [1000], @@ -53,6 +55,7 @@ const DEFAULT_VALUES = { const expressionFieldsWithValidation = [ 'index', 'esQuery', + 'size', 'timeField', 'threshold0', 'threshold1', @@ -74,6 +77,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< index, timeField, esQuery, + size, thresholdComparator, threshold, timeWindowSize, @@ -83,6 +87,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< const getDefaultParams = () => ({ ...alertParams, esQuery: esQuery ?? DEFAULT_VALUES.QUERY, + size: size ?? DEFAULT_VALUES.SIZE, timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, @@ -214,7 +219,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
    @@ -234,6 +239,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< ...alertParams, index: indices, esQuery: DEFAULT_VALUES.QUERY, + size: DEFAULT_VALUES.SIZE, thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR, timeWindowSize: DEFAULT_VALUES.TIME_WINDOW_SIZE, timeWindowUnit: DEFAULT_VALUES.TIME_WINDOW_UNIT, @@ -246,6 +252,19 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< }} onTimeFieldChange={(updatedTimeField: string) => setParam('timeField', updatedTimeField)} /> + { + setParam('size', updatedValue); + }} + />
    diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts index a22af7a7bc8a5..af34b88ba28c5 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts @@ -17,6 +17,7 @@ export interface EsQueryAlertParams extends AlertTypeParams { index: string[]; timeField?: string; esQuery: string; + size: number; thresholdComparator?: string; threshold: number[]; timeWindowSize: number; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts index 7d604e964fb9d..52278b4576557 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts @@ -13,6 +13,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: [], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], @@ -25,6 +26,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], @@ -37,6 +39,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`, + size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], @@ -49,6 +52,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"aggs\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], @@ -61,6 +65,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, threshold: [], timeWindowSize: 1, timeWindowUnit: 's', @@ -74,6 +79,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, threshold: [1], timeWindowSize: 1, timeWindowUnit: 's', @@ -87,6 +93,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, threshold: [10, 1], timeWindowSize: 1, timeWindowUnit: 's', @@ -97,4 +104,34 @@ describe('expression params validation', () => { 'Threshold 1 must be > Threshold 0.' ); }); + + test('if size property is < 0 should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: ['test'], + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`, + size: -1, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + }; + expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.size[0]).toBe( + 'Size must be between 0 and 10,000.' + ); + }); + + test('if size property is > 10000 should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: ['test'], + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`, + size: 25000, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + }; + expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.size[0]).toBe( + 'Size must be between 0 and 10,000.' + ); + }); }); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts index 8b402d63ae565..e6449dd4a6089 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts @@ -10,12 +10,21 @@ import { EsQueryAlertParams } from './types'; import { ValidationResult, builtInComparators } from '../../../../triggers_actions_ui/public'; export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => { - const { index, timeField, esQuery, threshold, timeWindowSize, thresholdComparator } = alertParams; + const { + index, + timeField, + esQuery, + size, + threshold, + timeWindowSize, + thresholdComparator, + } = alertParams; const validationResult = { errors: {} }; const errors = { index: new Array(), timeField: new Array(), esQuery: new Array(), + size: new Array(), threshold0: new Array(), threshold1: new Array(), thresholdComparator: new Array(), @@ -94,5 +103,20 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR }) ); } + if (!size) { + errors.size.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredSizeText', { + defaultMessage: 'Size is required.', + }) + ); + } + if ((size && size < 0) || size > 10000) { + errors.size.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.invalidSizeRangeText', { + defaultMessage: 'Size must be between 0 and {max, number}.', + values: { max: 10000 }, + }) + ); + } return validationResult; }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts index d4b2029c11579..9d4edd83a3913 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts @@ -14,6 +14,7 @@ describe('ActionContext', () => { index: ['[index]'], timeField: '[timeField]', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 5, timeWindowUnit: 'm', thresholdComparator: '>', @@ -41,6 +42,7 @@ describe('ActionContext', () => { index: ['[index]'], timeField: '[timeField]', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 5, timeWindowUnit: 'm', thresholdComparator: 'between', diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index 2049f9f1153dd..c38dad5134373 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -57,6 +57,10 @@ describe('alertType', () => { "description": "The string representation of the ES query.", "name": "esQuery", }, + Object { + "description": "The number of hits to retrieve for each query.", + "name": "size", + }, Object { "description": "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.", "name": "threshold", @@ -75,6 +79,7 @@ describe('alertType', () => { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 5, timeWindowUnit: 'm', thresholdComparator: '<', @@ -92,6 +97,7 @@ describe('alertType', () => { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 5, timeWindowUnit: 'm', thresholdComparator: 'between', diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 51c1fc4073d60..8fe988d95d72f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -23,8 +23,6 @@ import { ESSearchHit } from '../../../../../typings/elasticsearch'; export const ES_QUERY_ID = '.es-query'; -const DEFAULT_MAX_HITS_PER_EXECUTION = 1000; - const ActionGroupId = 'query matched'; const ConditionMetAlertInstanceId = 'query matched'; @@ -88,6 +86,13 @@ export function getAlertType( } ); + const actionVariableContextSizeLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextSizeLabel', + { + defaultMessage: 'The number of hits to retrieve for each query.', + } + ); + const actionVariableContextThresholdLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel', { @@ -130,6 +135,7 @@ export function getAlertType( params: [ { name: 'index', description: actionVariableContextIndexLabel }, { name: 'esQuery', description: actionVariableContextQueryLabel }, + { name: 'size', description: actionVariableContextSizeLabel }, { name: 'threshold', description: actionVariableContextThresholdLabel }, { name: 'thresholdComparator', description: actionVariableContextThresholdComparatorLabel }, ], @@ -160,7 +166,7 @@ export function getAlertType( } // During each alert execution, we run the configured query, get a hit count - // (hits.total) and retrieve up to DEFAULT_MAX_HITS_PER_EXECUTION hits. We + // (hits.total) and retrieve up to params.size hits. We // evaluate the threshold condition using the value of hits.total. If the threshold // condition is met, the hits are counted toward the query match and we update // the alert state with the timestamp of the latest hit. In the next execution @@ -200,7 +206,7 @@ export function getAlertType( from: dateStart, to: dateEnd, filter, - size: DEFAULT_MAX_HITS_PER_EXECUTION, + size: params.size, sortOrder: 'desc', searchAfterSortId: undefined, timeField: params.timeField, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts index a1a697446ff65..ab3ca6a2d4c31 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts @@ -7,12 +7,17 @@ import { TypeOf } from '@kbn/config-schema'; import type { Writable } from '@kbn/utility-types'; -import { EsQueryAlertParamsSchema, EsQueryAlertParams } from './alert_type_params'; +import { + EsQueryAlertParamsSchema, + EsQueryAlertParams, + ES_QUERY_MAX_HITS_PER_EXECUTION, +} from './alert_type_params'; const DefaultParams: Writable> = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 5, timeWindowUnit: 'm', thresholdComparator: '>', @@ -99,6 +104,28 @@ describe('alertType Params validate()', () => { ); }); + it('fails for invalid size', async () => { + delete params.size; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[size]: expected value of type [number] but got [undefined]"` + ); + + params.size = 'foo'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[size]: expected value of type [number] but got [string]"` + ); + + params.size = -1; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[size]: Value must be equal to or greater than [0]."` + ); + + params.size = ES_QUERY_MAX_HITS_PER_EXECUTION + 1; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[size]: Value must be equal to or lower than [10000]."` + ); + }); + it('fails for invalid timeWindowSize', async () => { delete params.timeWindowSize; expect(onValidate()).toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts index 24fed92776b53..23f314b521511 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts @@ -11,6 +11,8 @@ import { ComparatorFnNames } from '../lib'; import { validateTimeWindowUnits } from '../../../../triggers_actions_ui/server'; import { AlertTypeState } from '../../../../alerts/server'; +export const ES_QUERY_MAX_HITS_PER_EXECUTION = 10000; + // alert type parameters export type EsQueryAlertParams = TypeOf; export interface EsQueryAlertState extends AlertTypeState { @@ -21,6 +23,7 @@ export const EsQueryAlertParamsSchemaProperties = { index: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), timeField: schema.string({ minLength: 1 }), esQuery: schema.string({ minLength: 1 }), + size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }), timeWindowSize: schema.number({ min: 1 }), timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts index bfcbba28b4bda..f975375adcb07 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts @@ -10,3 +10,4 @@ export { OfExpression } from './of'; export { GroupByExpression } from './group_by_over'; export { ThresholdExpression } from './threshold'; export { ForLastExpression } from './for_the_last'; +export { ValueExpression } from './value'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx new file mode 100644 index 0000000000000..e9a3dce84e149 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { ValueExpression } from './value'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; + +describe('value expression', () => { + it('renders description and value', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="valueFieldTitle"]')).toMatchInlineSnapshot(` + + test + + `); + expect(wrapper.find('[data-test-subj="valueFieldNumberForm"]')).toMatchInlineSnapshot(` + + + + `); + }); + + it('renders errors', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="valueFieldNumberForm"]')).toMatchInlineSnapshot(` + + + + `); + }); + + it('renders closed popover initially and opens on click', async () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="valueExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="valueFieldTitle"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="valueFieldNumber"]').exists()).toBeFalsy(); + + wrapper.find('[data-test-subj="valueExpression"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="valueFieldTitle"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="valueFieldNumber"]').exists()).toBeTruthy(); + }); + + it('emits onChangeSelectedValue action when value is updated', async () => { + const onChangeSelectedValue = jest.fn(); + const wrapper = mountWithIntl( + + ); + + wrapper.find('[data-test-subj="valueExpression"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper + .find('input[data-test-subj="valueFieldNumber"]') + .simulate('change', { target: { value: 3000 } }); + expect(onChangeSelectedValue).toHaveBeenCalledWith(3000); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx new file mode 100644 index 0000000000000..cdf57136fe4b2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiExpression, + EuiPopover, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, +} from '@elastic/eui'; +import { ClosablePopoverTitle } from './components'; +import { IErrorObject } from '../../types'; + +interface ValueExpressionProps { + description: string; + value: number; + onChangeSelectedValue: (updatedValue: number) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; + display?: 'fullWidth' | 'inline'; + errors: string | string[] | IErrorObject; +} + +export const ValueExpression = ({ + description, + value, + onChangeSelectedValue, + display = 'inline', + popupPosition, + errors, +}: ValueExpressionProps) => { + const [valuePopoverOpen, setValuePopoverOpen] = useState(false); + return ( + { + setValuePopoverOpen(true); + }} + /> + } + isOpen={valuePopoverOpen} + closePopover={() => { + setValuePopoverOpen(false); + }} + ownFocus + display={display === 'fullWidth' ? 'block' : 'inlineBlock'} + anchorPosition={popupPosition ?? 'downLeft'} + repositionOnScroll + > +
    + setValuePopoverOpen(false)} + > + <>{description} + + + + 0 && value !== undefined} + error={errors} + > + 0 && value !== undefined} + onChange={(e: any) => { + onChangeSelectedValue(e.target.value as number); + }} + /> + + + +
    +
    + ); +}; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts index 30fd3aea2b2dc..777caacd465d8 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -68,6 +68,7 @@ export default function alertTests({ getService }: FtrProviderContext) { await createAlert({ name: 'never fire', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, thresholdComparator: '<', threshold: [0], }); @@ -75,6 +76,7 @@ export default function alertTests({ getService }: FtrProviderContext) { await createAlert({ name: 'always fire', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, thresholdComparator: '>', threshold: [-1], }); @@ -123,6 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) { await createAlert({ name: 'never fire', esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)), + size: 100, thresholdComparator: '>=', threshold: [0], }); @@ -132,6 +135,7 @@ export default function alertTests({ getService }: FtrProviderContext) { esQuery: JSON.stringify( rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2)) ), + size: 100, thresholdComparator: '>=', threshold: [0], }); @@ -173,6 +177,7 @@ export default function alertTests({ getService }: FtrProviderContext) { name: string; timeField?: string; esQuery: string; + size: number; thresholdComparator: string; threshold: number[]; timeWindowSize?: number; @@ -215,6 +220,7 @@ export default function alertTests({ getService }: FtrProviderContext) { index: [ES_TEST_INDEX_NAME], timeField: params.timeField || 'date', esQuery: params.esQuery, + size: params.size, timeWindowSize: params.timeWindowSize || ALERT_INTERVAL_SECONDS * 5, timeWindowUnit: 's', thresholdComparator: params.thresholdComparator, From 6dde543b6c6a34fa025a6d6a8f0f5a7bb7d78a95 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 9 Feb 2021 12:26:28 -0700 Subject: [PATCH 069/100] TS project refs: Converts rollup, remoteClusters, crossClusterReplication, indexLifecycleManagement to a TS project refs (#90713) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cross_cluster_replication/tsconfig.json | 31 ++++++++++++++++ .../index_lifecycle_management/tsconfig.json | 32 +++++++++++++++++ .../server/routes/api/add_route.test.ts | 15 +++++++- .../server/routes/api/delete_route.test.ts | 15 +++++++- .../server/routes/api/get_route.test.ts | 15 +++++++- .../server/routes/api/update_route.test.ts | 15 +++++++- x-pack/plugins/remote_clusters/tsconfig.json | 30 ++++++++++++++++ x-pack/plugins/rollup/tsconfig.json | 35 +++++++++++++++++++ x-pack/test/tsconfig.json | 7 ++-- x-pack/tsconfig.json | 8 +++++ x-pack/tsconfig.refs.json | 4 +++ 11 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/cross_cluster_replication/tsconfig.json create mode 100644 x-pack/plugins/index_lifecycle_management/tsconfig.json create mode 100644 x-pack/plugins/remote_clusters/tsconfig.json create mode 100644 x-pack/plugins/rollup/tsconfig.json diff --git a/x-pack/plugins/cross_cluster_replication/tsconfig.json b/x-pack/plugins/cross_cluster_replication/tsconfig.json new file mode 100644 index 0000000000000..9c7590b9c2553 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + // required plugins + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../remote_clusters/tsconfig.json" }, + { "path": "../index_management/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + // optional plugins + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + // required bundles + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/index_lifecycle_management/tsconfig.json b/x-pack/plugins/index_lifecycle_management/tsconfig.json new file mode 100644 index 0000000000000..73dcc62132cbf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/tsconfig.json @@ -0,0 +1,32 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "__jest__/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + "../../typings/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + // required plugins + { "path": "../licensing/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" }, + // optional plugins + { "path": "../cloud/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../index_management/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + // required bundles + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts index 066a2d56cbeec..9348fd1eb20df 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts @@ -10,13 +10,26 @@ import { register } from './add_route'; import { API_BASE_PATH } from '../../../common/constants'; import { LicenseStatus } from '../../types'; -import { xpackMocks } from '../../../../../mocks'; +import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; + import { elasticsearchServiceMock, httpServerMock, httpServiceMock, + coreMock, } from '../../../../../../src/core/server/mocks'; +// Re-implement the mock that was imported directly from `x-pack/mocks` +function createCoreRequestHandlerContextMock() { + return { + core: coreMock.createRequestHandlerContext(), + licensing: licensingMock.createRequestHandlerContext(), + }; +} + +const xpackMocks = { + createRequestHandlerContext: createCoreRequestHandlerContextMock, +}; interface TestOptions { licenseCheckResult?: LicenseStatus; apiResponses?: Array<() => Promise>; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts index 29d846314bd9b..ce94f45bb8443 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts @@ -10,13 +10,26 @@ import { register } from './delete_route'; import { API_BASE_PATH } from '../../../common/constants'; import { LicenseStatus } from '../../types'; -import { xpackMocks } from '../../../../../mocks'; +import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; + import { elasticsearchServiceMock, httpServerMock, httpServiceMock, + coreMock, } from '../../../../../../src/core/server/mocks'; +// Re-implement the mock that was imported directly from `x-pack/mocks` +function createCoreRequestHandlerContextMock() { + return { + core: coreMock.createRequestHandlerContext(), + licensing: licensingMock.createRequestHandlerContext(), + }; +} + +const xpackMocks = { + createRequestHandlerContext: createCoreRequestHandlerContextMock, +}; interface TestOptions { licenseCheckResult?: LicenseStatus; apiResponses?: Array<() => Promise>; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index 33a3142ddc105..25d17d796b0ee 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -12,13 +12,26 @@ import { register } from './get_route'; import { API_BASE_PATH } from '../../../common/constants'; import { LicenseStatus } from '../../types'; -import { xpackMocks } from '../../../../../mocks'; +import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; + import { elasticsearchServiceMock, httpServerMock, httpServiceMock, + coreMock, } from '../../../../../../src/core/server/mocks'; +// Re-implement the mock that was imported directly from `x-pack/mocks` +function createCoreRequestHandlerContextMock() { + return { + core: coreMock.createRequestHandlerContext(), + licensing: licensingMock.createRequestHandlerContext(), + }; +} + +const xpackMocks = { + createRequestHandlerContext: createCoreRequestHandlerContextMock, +}; interface TestOptions { licenseCheckResult?: LicenseStatus; apiResponses?: Array<() => Promise>; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts index 31db362f7c953..22c87786a585c 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts @@ -10,13 +10,26 @@ import { register } from './update_route'; import { API_BASE_PATH } from '../../../common/constants'; import { LicenseStatus } from '../../types'; -import { xpackMocks } from '../../../../../mocks'; +import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; + import { elasticsearchServiceMock, httpServerMock, httpServiceMock, + coreMock, } from '../../../../../../src/core/server/mocks'; +// Re-implement the mock that was imported directly from `x-pack/mocks` +function createCoreRequestHandlerContextMock() { + return { + core: coreMock.createRequestHandlerContext(), + licensing: licensingMock.createRequestHandlerContext(), + }; +} + +const xpackMocks = { + createRequestHandlerContext: createCoreRequestHandlerContextMock, +}; interface TestOptions { licenseCheckResult?: LicenseStatus; apiResponses?: Array<() => Promise>; diff --git a/x-pack/plugins/remote_clusters/tsconfig.json b/x-pack/plugins/remote_clusters/tsconfig.json new file mode 100644 index 0000000000000..0bee6300cf0b2 --- /dev/null +++ b/x-pack/plugins/remote_clusters/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "fixtures/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + // required plugins + { "path": "../licensing/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../index_management/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + // optional plugins + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../cloud/tsconfig.json" }, + // required bundles + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/rollup/tsconfig.json b/x-pack/plugins/rollup/tsconfig.json new file mode 100644 index 0000000000000..9b994d1710ffc --- /dev/null +++ b/x-pack/plugins/rollup/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "fixtures/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + // required plugins + { "path": "../../../src/plugins/index_pattern_management/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + // optional plugins + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../index_management/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/vis_type_timeseries/tsconfig.json" }, + // required bundles + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 7ba5c00a71b37..4cbec2da21807 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -82,9 +82,10 @@ { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../plugins/upgrade_assistant/tsconfig.json" }, { "path": "../plugins/watcher/tsconfig.json" }, - { "path": "../plugins/runtime_fields/tsconfig.json" }, - { "path": "../plugins/index_management/tsconfig.json" }, - { "path": "../plugins/watcher/tsconfig.json" }, + { "path": "../plugins/rollup/tsconfig.json" }, + { "path": "../plugins/remote_clusters/tsconfig.json" }, + { "path": "../plugins/cross_cluster_replication/tsconfig.json" }, + { "path": "../plugins/index_lifecycle_management/tsconfig.json"}, { "path": "../plugins/uptime/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 3afbb027e7fde..f4497487f6ff9 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -57,6 +57,10 @@ "plugins/index_management/**/*", "plugins/grokdebugger/**/*", "plugins/upgrade_assistant/**/*", + "plugins/rollup/**/*", + "plugins/remote_clusters/**/*", + "plugins/cross_cluster_replication/**/*", + "plugins/index_lifecycle_management/**/*", "plugins/uptime/**/*", "test/**/*" ], @@ -148,6 +152,10 @@ { "path": "./plugins/runtime_fields/tsconfig.json" }, { "path": "./plugins/index_management/tsconfig.json" }, { "path": "./plugins/watcher/tsconfig.json" }, + { "path": "./plugins/rollup/tsconfig.json" }, + { "path": "./plugins/remote_clusters/tsconfig.json" }, + { "path": "./plugins/cross_cluster_replication/tsconfig.json"}, + { "path": "./plugins/index_lifecycle_management/tsconfig.json"}, { "path": "./plugins/uptime/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index 54cee9b124237..d4d7e8caa5088 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -52,6 +52,10 @@ { "path": "./plugins/runtime_fields/tsconfig.json" }, { "path": "./plugins/index_management/tsconfig.json" }, { "path": "./plugins/watcher/tsconfig.json" }, + { "path": "./plugins/rollup/tsconfig.json"}, + { "path": "./plugins/remote_clusters/tsconfig.json"}, + { "path": "./plugins/cross_cluster_replication/tsconfig.json"}, + { "path": "./plugins/index_lifecycle_management/tsconfig.json"}, { "path": "./plugins/uptime/tsconfig.json" } ] } From f939334b6ba53998eead3fbfb1f94a2214b0946f Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Tue, 9 Feb 2021 14:38:13 -0500 Subject: [PATCH 070/100] [Index Management][Datastreams] Fixed datastreams functional app load test to check for visibility of element to prevent false positives (#85708) --- x-pack/test/functional/apps/index_management/home_page.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts index 9fbe5373dd3d4..f8f852b9667fb 100644 --- a/x-pack/test/functional/apps/index_management/home_page.ts +++ b/x-pack/test/functional/apps/index_management/home_page.ts @@ -13,6 +13,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'indexManagement', 'header']); const log = getService('log'); const browser = getService('browser'); + const retry = getService('retry'); describe('Home page', function () { before(async () => { @@ -49,8 +50,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(url).to.contain(`/data_streams`); // Verify content - const dataStreamList = await testSubjects.exists('dataStreamList'); - expect(dataStreamList).to.be(true); + await retry.waitFor('Wait until dataStream Table is visible.', async () => { + return await testSubjects.isDisplayed('dataStreamTable'); + }); }); }); From f6b6a8219b8c6dfdf504f4b399086f5315315e53 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 9 Feb 2021 14:52:41 -0500 Subject: [PATCH 071/100] [ML] Data Frame Analytics: Support early stopping data frame analytics job parameter (#90695) * add support for early_stopping_enabled in cloning * remove obsolete comment --- .../components/action_clone/clone_action_name.tsx | 8 ++++++++ .../hooks/use_create_analytics_form/state.ts | 7 ++++++- .../functional/apps/ml/data_frame_analytics/cloning.ts | 3 +-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx index 039a00afe52ee..ee66612de97ac 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx @@ -117,6 +117,10 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo optional: true, defaultValue: 'maximize_minimum_recall', }, + early_stopping_enabled: { + optional: true, + ignore: true, + }, }, } : {}), @@ -207,6 +211,10 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo loss_function_parameter: { optional: true, }, + early_stopping_enabled: { + optional: true, + ignore: true, + }, }, } : {}), diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 8de4470b028f5..a70962c45ffcb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -56,6 +56,7 @@ export interface State { destinationIndexNameEmpty: boolean; destinationIndexNameValid: boolean; destinationIndexPatternTitleExists: boolean; + earlyStoppingEnabled: undefined | boolean; eta: undefined | number; featureBagFraction: undefined | number; featureInfluenceThreshold: undefined | number; @@ -125,6 +126,7 @@ export const getInitialState = (): State => ({ destinationIndexNameEmpty: true, destinationIndexNameValid: false, destinationIndexPatternTitleExists: false, + earlyStoppingEnabled: undefined, eta: undefined, featureBagFraction: undefined, featureInfluenceThreshold: undefined, @@ -239,7 +241,10 @@ export const getJobConfigFromFormState = ( formState.gamma && { gamma: formState.gamma }, formState.lambda && { lambda: formState.lambda }, formState.maxTrees && { max_trees: formState.maxTrees }, - formState.randomizeSeed && { randomize_seed: formState.randomizeSeed } + formState.randomizeSeed && { randomize_seed: formState.randomizeSeed }, + formState.earlyStoppingEnabled !== undefined && { + early_stopping_enabled: formState.earlyStoppingEnabled, + } ); jobConfig.analysis = { diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 72e81dad44629..04712fc0c1426 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -15,8 +15,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - // Failing ES promotion, see https://github.com/elastic/kibana/issues/89980 - describe.skip('jobs cloning supported by UI form', function () { + describe('jobs cloning supported by UI form', function () { const testDataList: Array<{ suiteTitle: string; archive: string; From 128488c6d11c1a14ff34ca8f03c4eebbfb2ac48c Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 9 Feb 2021 15:20:58 -0500 Subject: [PATCH 072/100] [Time to Visualize] Clear Unsaved Changes When Dashboard Fails to Load (#90527) * added error handling to dashboard_unsaved_listing. It should now remove all unsaved changes from dashboards which error on load --- .../listing/dashboard_listing.test.tsx | 8 ++- .../application/listing/dashboard_listing.tsx | 22 ++++-- .../dashboard_unsaved_listing.test.tsx | 69 +++++++++++++------ .../listing/dashboard_unsaved_listing.tsx | 66 ++++++++++++------ 4 files changed, 116 insertions(+), 49 deletions(-) diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx index ee731db0ced65..c7d5db970db42 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx @@ -49,11 +49,16 @@ function makeDefaultServices(): DashboardAppServices { hits, }); }; + const dashboardPanelStorage = ({ + getDashboardIdsWithUnsavedChanges: jest + .fn() + .mockResolvedValue(['dashboardUnsavedOne', 'dashboardUnsavedTwo']), + } as unknown) as DashboardPanelStorage; + return { savedObjects: savedObjectsPluginMock.createStartContract(), embeddable: embeddablePluginMock.createInstance().doStart(), dashboardCapabilities: {} as DashboardCapabilities, - dashboardPanelStorage: {} as DashboardPanelStorage, initializerContext: {} as PluginInitializerContext, chrome: chromeServiceMock.createStartContract(), navigation: {} as NavigationPublicPluginStart, @@ -68,6 +73,7 @@ function makeDefaultServices(): DashboardAppServices { restorePreviousUrl: () => {}, onAppLeave: (handler) => {}, allowByValueEmbeddables: true, + dashboardPanelStorage, savedDashboards, core, }; diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index 24d08ad06cc3b..c12385a29c4ec 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -8,7 +8,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; -import React, { Fragment, useCallback, useEffect, useMemo } from 'react'; +import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { attemptLoadDashboardByTitle } from '../lib'; import { DashboardAppServices, DashboardRedirect } from '../types'; import { getDashboardBreadcrumb, dashboardListingTable } from '../../dashboard_strings'; @@ -48,6 +48,10 @@ export const DashboardListing = ({ }, } = useKibana(); + const [unsavedDashboardIds, setUnsavedDashboardIds] = useState( + dashboardPanelStorage.getDashboardIdsWithUnsavedChanges() + ); + // Set breadcrumbs useEffect useEffect(() => { setBreadcrumbs([ @@ -135,8 +139,12 @@ export const DashboardListing = ({ ); const deleteItems = useCallback( - (dashboards: Array<{ id: string }>) => savedDashboards.delete(dashboards.map((d) => d.id)), - [savedDashboards] + (dashboards: Array<{ id: string }>) => { + dashboards.map((d) => dashboardPanelStorage.clearPanels(d.id)); + setUnsavedDashboardIds(dashboardPanelStorage.getDashboardIdsWithUnsavedChanges()); + return savedDashboards.delete(dashboards.map((d) => d.id)); + }, + [savedDashboards, dashboardPanelStorage] ); const editItem = useCallback( @@ -179,7 +187,13 @@ export const DashboardListing = ({ tableColumns, }} > - + + setUnsavedDashboardIds(dashboardPanelStorage.getDashboardIdsWithUnsavedChanges()) + } + /> ); }; diff --git a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.test.tsx b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.test.tsx index 119b2d559b68a..13688b4061be9 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.test.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.test.tsx @@ -17,8 +17,8 @@ import { KibanaContextProvider } from '../../services/kibana_react'; import { SavedObjectLoader } from '../../services/saved_objects'; import { DashboardPanelStorage } from '../lib'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; -import { DashboardAppServices, DashboardRedirect } from '../types'; -import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; +import { DashboardAppServices } from '../types'; +import { DashboardUnsavedListing, DashboardUnsavedListingProps } from './dashboard_unsaved_listing'; const mockedDashboards: { [key: string]: DashboardSavedObject } = { dashboardUnsavedOne: { @@ -39,16 +39,11 @@ function makeDefaultServices(): DashboardAppServices { const core = coreMock.createStart(); core.overlays.openConfirm = jest.fn().mockResolvedValue(true); const savedDashboards = {} as SavedObjectLoader; - savedDashboards.get = jest.fn().mockImplementation((id: string) => mockedDashboards[id]); + savedDashboards.get = jest + .fn() + .mockImplementation((id: string) => Promise.resolve(mockedDashboards[id])); const dashboardPanelStorage = {} as DashboardPanelStorage; dashboardPanelStorage.clearPanels = jest.fn(); - dashboardPanelStorage.getDashboardIdsWithUnsavedChanges = jest - .fn() - .mockImplementation(() => [ - 'dashboardUnsavedOne', - 'dashboardUnsavedTwo', - 'dashboardUnsavedThree', - ]); return ({ dashboardPanelStorage, savedDashboards, @@ -56,14 +51,18 @@ function makeDefaultServices(): DashboardAppServices { } as unknown) as DashboardAppServices; } -const makeDefaultProps = () => ({ redirectTo: jest.fn() }); +const makeDefaultProps = (): DashboardUnsavedListingProps => ({ + redirectTo: jest.fn(), + unsavedDashboardIds: ['dashboardUnsavedOne', 'dashboardUnsavedTwo', 'dashboardUnsavedThree'], + refreshUnsavedDashboards: jest.fn(), +}); function mountWith({ services: incomingServices, props: incomingProps, }: { services?: DashboardAppServices; - props?: { redirectTo: DashboardRedirect }; + props?: DashboardUnsavedListingProps; }) { const services = incomingServices ?? makeDefaultServices(); const props = incomingProps ?? makeDefaultProps(); @@ -89,11 +88,9 @@ describe('Unsaved listing', () => { }); it('Does not attempt to get unsaved dashboard id', async () => { - const services = makeDefaultServices(); - services.dashboardPanelStorage.getDashboardIdsWithUnsavedChanges = jest - .fn() - .mockImplementation(() => ['dashboardUnsavedOne', DASHBOARD_PANELS_UNSAVED_ID]); - mountWith({ services }); + const props = makeDefaultProps(); + props.unsavedDashboardIds = ['dashboardUnsavedOne', DASHBOARD_PANELS_UNSAVED_ID]; + const { services } = mountWith({ props }); await waitFor(() => { expect(services.savedDashboards.get).toHaveBeenCalledTimes(1); }); @@ -115,11 +112,9 @@ describe('Unsaved listing', () => { }); it('Redirects to new dashboard when continue editing clicked', async () => { - const services = makeDefaultServices(); - services.dashboardPanelStorage.getDashboardIdsWithUnsavedChanges = jest - .fn() - .mockImplementation(() => [DASHBOARD_PANELS_UNSAVED_ID]); - const { props, component } = mountWith({ services }); + const props = makeDefaultProps(); + props.unsavedDashboardIds = [DASHBOARD_PANELS_UNSAVED_ID]; + const { component } = mountWith({ props }); const getEditButton = () => findTestSubject(component, `edit-unsaved-New-Dashboard`); await waitFor(() => { component.update(); @@ -150,4 +145,34 @@ describe('Unsaved listing', () => { ); }); }); + + it('removes unsaved changes from any dashboard which errors on fetch', async () => { + const services = makeDefaultServices(); + const props = makeDefaultProps(); + services.savedDashboards.get = jest.fn().mockImplementation((id: string) => { + if (id === 'failCase1' || id === 'failCase2') { + return Promise.reject(new Error()); + } + return Promise.resolve(mockedDashboards[id]); + }); + + props.unsavedDashboardIds = [ + 'dashboardUnsavedOne', + 'dashboardUnsavedTwo', + 'dashboardUnsavedThree', + 'failCase1', + 'failCase2', + ]; + const { component } = mountWith({ services, props }); + waitFor(() => { + component.update(); + expect(services.dashboardPanelStorage.clearPanels).toHaveBeenCalledWith('failCase1'); + expect(services.dashboardPanelStorage.clearPanels).toHaveBeenCalledWith('failCase2'); + + // clearing panels from dashboard with errors should cause getDashboardIdsWithUnsavedChanges to be called again. + expect( + services.dashboardPanelStorage.getDashboardIdsWithUnsavedChanges + ).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx index d7b9564d9d1e3..db50cfb638d64 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx @@ -106,7 +106,17 @@ interface UnsavedItemMap { [key: string]: DashboardSavedObject; } -export const DashboardUnsavedListing = ({ redirectTo }: { redirectTo: DashboardRedirect }) => { +export interface DashboardUnsavedListingProps { + refreshUnsavedDashboards: () => void; + redirectTo: DashboardRedirect; + unsavedDashboardIds: string[]; +} + +export const DashboardUnsavedListing = ({ + redirectTo, + unsavedDashboardIds, + refreshUnsavedDashboards, +}: DashboardUnsavedListingProps) => { const { services: { dashboardPanelStorage, @@ -116,9 +126,6 @@ export const DashboardUnsavedListing = ({ redirectTo }: { redirectTo: DashboardR } = useKibana(); const [items, setItems] = useState({}); - const [dashboardIds, setDashboardIds] = useState( - dashboardPanelStorage.getDashboardIdsWithUnsavedChanges() - ); const onOpen = useCallback( (id?: string) => { @@ -133,48 +140,63 @@ export const DashboardUnsavedListing = ({ redirectTo }: { redirectTo: DashboardR overlays, () => { dashboardPanelStorage.clearPanels(id); - setDashboardIds(dashboardPanelStorage.getDashboardIdsWithUnsavedChanges()); + refreshUnsavedDashboards(); }, createConfirmStrings.getCancelButtonText() ); }, - [overlays, dashboardPanelStorage] + [overlays, refreshUnsavedDashboards, dashboardPanelStorage] ); useEffect(() => { - if (dashboardIds?.length === 0) { + if (unsavedDashboardIds?.length === 0) { return; } let canceled = false; - const dashPromises = dashboardIds + const dashPromises = unsavedDashboardIds .filter((id) => id !== DASHBOARD_PANELS_UNSAVED_ID) - .map((dashboardId) => savedDashboards.get(dashboardId)); - Promise.all(dashPromises).then((dashboards: DashboardSavedObject[]) => { + .map((dashboardId) => { + return (savedDashboards.get(dashboardId) as Promise).catch( + () => dashboardId + ); + }); + Promise.all(dashPromises).then((dashboards: Array) => { const dashboardMap = {}; if (canceled) { return; } - setItems( - dashboards.reduce((map, dashboard) => { - return { - ...map, - [dashboard.id || DASHBOARD_PANELS_UNSAVED_ID]: dashboard, - }; - }, dashboardMap) - ); + let hasError = false; + const newItems = dashboards.reduce((map, dashboard) => { + if (typeof dashboard === 'string') { + hasError = true; + dashboardPanelStorage.clearPanels(dashboard); + return map; + } + return { + ...map, + [dashboard.id || DASHBOARD_PANELS_UNSAVED_ID]: dashboard, + }; + }, dashboardMap); + if (hasError) { + refreshUnsavedDashboards(); + return; + } + setItems(newItems); }); return () => { canceled = true; }; - }, [dashboardIds, savedDashboards]); + }, [savedDashboards, dashboardPanelStorage, refreshUnsavedDashboards, unsavedDashboardIds]); - return dashboardIds.length === 0 ? null : ( + return unsavedDashboardIds.length === 0 ? null : ( <> 1)} + title={dashboardUnsavedListingStrings.getUnsavedChangesTitle( + unsavedDashboardIds.length > 1 + )} > - {dashboardIds.map((dashboardId: string) => { + {unsavedDashboardIds.map((dashboardId: string) => { const title: string | undefined = dashboardId === DASHBOARD_PANELS_UNSAVED_ID ? getNewDashboardTitle() From de3f4aa419c944c8c22234b82d7b60214423a7e7 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 9 Feb 2021 12:21:29 -0800 Subject: [PATCH 073/100] [docs/ci-stats] describe new metrics files (#90711) Co-authored-by: spalger --- .../contributing/development-ci-metrics.asciidoc | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/developer/contributing/development-ci-metrics.asciidoc b/docs/developer/contributing/development-ci-metrics.asciidoc index 3e49686fb67f0..2efe4e7c60a7d 100644 --- a/docs/developer/contributing/development-ci-metrics.asciidoc +++ b/docs/developer/contributing/development-ci-metrics.asciidoc @@ -121,13 +121,20 @@ Changes to the {kib-repo}blob/{branch}/packages/kbn-optimizer/limits.yml[`limits [[ci-metric-validating-limits]] === Validating `page load bundle size` limits -Once you've fixed any issues discovered while diagnosing overages you probably should just push the changes to your PR and let CI validate them. +While you're trying to track down changes which will improve the bundle size, try running the following command locally: -If you have a pretty powerful dev machine, or the necessary patience/determination, you can validate the limits locally by running the following command: +[source,shell] +----------- +node scripts/build_kibana_platform_plugins --dist --watch --focus {pluginId} +----------- + +This will build the front-end bundles for your plugin and only the plugins your plugin depends on. Whenever you make changes the bundles are rebuilt and you can inspect the metrics of that build in the `target/public/metrics.json` file within your plugin. This file will be updated as you save changes to the source and should be helpful to determine if your changes are lowering the `page load asset size` enough. + +If you only want to run the build once you can run: [source,shell] ----------- -node scripts/build_kibana_platform_plugins --validate-limits +node scripts/build_kibana_platform_plugins --validate-limits --focus {pluginId} ----------- This command needs to apply production optimizations to get the right sizes, which means that the optimizer will take significantly longer to run and on most developmer machines will consume all of your machines resources for 20 minutes or more. If you'd like to multi-task while this is running you might need to limit the number of workers using the `--max-workers` flag. \ No newline at end of file From 5a27e69c6b6ec92f0c47dfece5027e8d3cc6413f Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 9 Feb 2021 13:35:37 -0700 Subject: [PATCH 074/100] [Security Solutions][Detection Engine] Unskips tests after ES promotion and adds deleteUserRole utility (#90533) ## Summary Unskips tests after a ES promotion and adds a delete user role utility. Ref: https://github.com/elastic/kibana/issues/90229 https://github.com/elastic/kibana/issues/88302 Removes one `any` from the utils by switching to using `ProvidedType` Before: Screen Shot 2021-02-05 at 2 45 37 PM After: Screen Shot 2021-02-05 at 4 13 23 PM Turns out that return types on overloaded functions aren't easy fwiw and will fall on the bottom one which in this case looked to be `any` which we don't want: https://github.com/Microsoft/TypeScript/issues/24275#issuecomment-390701982 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../roles_users_utils/index.ts | 40 +++++++--- .../security_and_spaces/tests/create_index.ts | 74 ++++++++++++++----- .../tests/create_signals_migrations.ts | 7 +- .../tests/delete_signals_migrations.ts | 8 +- .../tests/finalize_signals_migrations.ts | 12 +-- .../tests/get_signals_migration_status.ts | 7 +- .../tests/open_close_signals.ts | 11 ++- .../tests/read_privileges.ts | 20 ++++- 8 files changed, 126 insertions(+), 53 deletions(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts index 1d3e5244a59ed..9e84ad0a547aa 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts @@ -6,6 +6,7 @@ */ import { assertUnreachable } from '../../../../plugins/security_solution/common/utility_types'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; import { t1AnalystUser, t2AnalystUser, @@ -26,10 +27,9 @@ import { } from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users'; import { ROLES } from '../../../../plugins/security_solution/common/test'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; export const createUserAndRole = async ( - securityService: ReturnType, + getService: FtrProviderContext['getService'], role: ROLES ): Promise => { switch (role) { @@ -38,32 +38,47 @@ export const createUserAndRole = async ( ROLES.detections_admin, detectionsAdminRole, detectionsAdminUser, - securityService + getService ); case ROLES.t1_analyst: - return postRoleAndUser(ROLES.t1_analyst, t1AnalystRole, t1AnalystUser, securityService); + return postRoleAndUser(ROLES.t1_analyst, t1AnalystRole, t1AnalystUser, getService); case ROLES.t2_analyst: - return postRoleAndUser(ROLES.t2_analyst, t2AnalystRole, t2AnalystUser, securityService); + return postRoleAndUser(ROLES.t2_analyst, t2AnalystRole, t2AnalystUser, getService); case ROLES.hunter: - return postRoleAndUser(ROLES.hunter, hunterRole, hunterUser, securityService); + return postRoleAndUser(ROLES.hunter, hunterRole, hunterUser, getService); case ROLES.rule_author: - return postRoleAndUser(ROLES.rule_author, ruleAuthorRole, ruleAuthorUser, securityService); + return postRoleAndUser(ROLES.rule_author, ruleAuthorRole, ruleAuthorUser, getService); case ROLES.soc_manager: - return postRoleAndUser(ROLES.soc_manager, socManagerRole, socManagerUser, securityService); + return postRoleAndUser(ROLES.soc_manager, socManagerRole, socManagerUser, getService); case ROLES.platform_engineer: return postRoleAndUser( ROLES.platform_engineer, platformEngineerRole, platformEngineerUser, - securityService + getService ); case ROLES.reader: - return postRoleAndUser(ROLES.reader, readerRole, readerUser, securityService); + return postRoleAndUser(ROLES.reader, readerRole, readerUser, getService); default: return assertUnreachable(role); } }; +/** + * Given a roleName and security service this will delete the roleName + * and user + * @param roleName The user and role to delete with the same name + * @param securityService The security service + */ +export const deleteUserAndRole = async ( + getService: FtrProviderContext['getService'], + roleName: ROLES +): Promise => { + const securityService = getService('security'); + await securityService.user.delete(roleName); + await securityService.role.delete(roleName); +}; + interface UserInterface { password: string; roles: string[]; @@ -95,8 +110,9 @@ export const postRoleAndUser = async ( roleName: string, role: RoleInterface, user: UserInterface, - securityService: ReturnType -) => { + getService: FtrProviderContext['getService'] +): Promise => { + const securityService = getService('security'); await securityService.role.create(roleName, { kibana: role.kibana, elasticsearch: role.elasticsearch, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts index c2822b8638d76..a319c30fa20de 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts @@ -14,16 +14,14 @@ import { import { FtrProviderContext } from '../../common/ftr_provider_context'; import { deleteSignalsIndex } from '../../utils'; import { ROLES } from '../../../../plugins/security_solution/common/test'; -import { createUserAndRole } from '../roles_users_utils'; +import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - const security = getService('security'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/90229 - describe.skip('create_index', () => { + describe('create_index', () => { afterEach(async () => { await deleteSignalsIndex(supertest); }); @@ -66,8 +64,13 @@ export default ({ getService }: FtrProviderContext) => { describe('t1_analyst', () => { const role = ROLES.t1_analyst; + beforeEach(async () => { - await createUserAndRole(security, role); + await createUserAndRole(getService, role); + }); + + afterEach(async () => { + await deleteUserAndRole(getService, role); }); it('should return a 404 when the signal index has never been created', async () => { @@ -88,7 +91,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(403); expect(body).to.eql({ message: - 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [t1_analyst], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]', + 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [t1_analyst], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]', status_code: 403, }); }); @@ -111,8 +114,13 @@ export default ({ getService }: FtrProviderContext) => { describe('t2_analyst', () => { const role = ROLES.t2_analyst; + beforeEach(async () => { - await createUserAndRole(security, role); + await createUserAndRole(getService, role); + }); + + afterEach(async () => { + await deleteUserAndRole(getService, role); }); it('should return a 404 when the signal index has never been created', async () => { @@ -133,7 +141,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(403); expect(body).to.eql({ message: - 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [t2_analyst], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]', + 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [t2_analyst], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]', status_code: 403, }); }); @@ -156,8 +164,13 @@ export default ({ getService }: FtrProviderContext) => { describe('detections_admin', () => { const role = ROLES.detections_admin; + beforeEach(async () => { - await createUserAndRole(security, role); + await createUserAndRole(getService, role); + }); + + afterEach(async () => { + await deleteUserAndRole(getService, role); }); it('should return a 404 when the signal index has never been created', async () => { @@ -201,8 +214,13 @@ export default ({ getService }: FtrProviderContext) => { describe('soc_manager', () => { const role = ROLES.soc_manager; + beforeEach(async () => { - await createUserAndRole(security, role); + await createUserAndRole(getService, role); + }); + + afterEach(async () => { + await deleteUserAndRole(getService, role); }); it('should return a 404 when the signal index has never been created', async () => { @@ -223,7 +241,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(403); expect(body).to.eql({ message: - 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [soc_manager], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]', + 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [soc_manager], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]', status_code: 403, }); }); @@ -246,8 +264,13 @@ export default ({ getService }: FtrProviderContext) => { describe('hunter', () => { const role = ROLES.hunter; + beforeEach(async () => { - await createUserAndRole(security, role); + await createUserAndRole(getService, role); + }); + + afterEach(async () => { + await deleteUserAndRole(getService, role); }); it('should return a 404 when the signal index has never been created', async () => { @@ -268,7 +291,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(403); expect(body).to.eql({ message: - 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [hunter], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]', + 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [hunter], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]', status_code: 403, }); }); @@ -291,8 +314,13 @@ export default ({ getService }: FtrProviderContext) => { describe('platform_engineer', () => { const role = ROLES.platform_engineer; + beforeEach(async () => { - await createUserAndRole(security, role); + await createUserAndRole(getService, role); + }); + + afterEach(async () => { + await deleteUserAndRole(getService, role); }); it('should return a 404 when the signal index has never been created', async () => { @@ -336,8 +364,13 @@ export default ({ getService }: FtrProviderContext) => { describe('reader', () => { const role = ROLES.reader; + beforeEach(async () => { - await createUserAndRole(security, role); + await createUserAndRole(getService, role); + }); + + afterEach(async () => { + await deleteUserAndRole(getService, role); }); it('should return a 404 when the signal index has never been created', async () => { @@ -358,7 +391,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(403); expect(body).to.eql({ message: - 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [reader], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]', + 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [reader], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]', status_code: 403, }); }); @@ -381,8 +414,13 @@ export default ({ getService }: FtrProviderContext) => { describe('rule_author', () => { const role = ROLES.rule_author; + beforeEach(async () => { - await createUserAndRole(security, role); + await createUserAndRole(getService, role); + }); + + afterEach(async () => { + await deleteUserAndRole(getService, role); }); it('should return a 404 when the signal index has never been created', async () => { @@ -403,7 +441,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(403); expect(body).to.eql({ message: - 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [rule_author], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]', + 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [rule_author], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]', status_code: 403, }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts index fc77fba5fa339..dd0052b03382a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts @@ -21,7 +21,7 @@ import { getIndexNameFromLoad, waitForIndexToPopulate, } from '../../utils'; -import { createUserAndRole } from '../roles_users_utils'; +import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils'; interface CreateResponse { index: string; @@ -34,7 +34,6 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); const esArchiver = getService('esArchiver'); const kbnClient = getService('kibanaServer'); - const security = getService('security'); const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -173,7 +172,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('rejects the request if the user does not have sufficient privileges', async () => { - await createUserAndRole(security, ROLES.t1_analyst); + await createUserAndRole(getService, ROLES.t1_analyst); await supertestWithoutAuth .post(DETECTION_ENGINE_SIGNALS_MIGRATION_URL) @@ -181,6 +180,8 @@ export default ({ getService }: FtrProviderContext): void => { .auth(ROLES.t1_analyst, 'changeme') .send({ index: [legacySignalsIndexName] }) .expect(400); + + await deleteUserAndRole(getService, ROLES.t1_analyst); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts index 234370e4f104e..bba6ce1125c37 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts @@ -32,12 +32,10 @@ interface FinalizeResponse extends CreateResponse { export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); const esArchiver = getService('esArchiver'); - const security = getService('security'); const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/90229 - describe.skip('deleting signals migrations', () => { + describe('deleting signals migrations', () => { let outdatedSignalsIndexName: string; let createdMigration: CreateResponse; let finalizedMigration: FinalizeResponse; @@ -105,7 +103,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('rejects the request if the user does not have sufficient privileges', async () => { - await createUserAndRole(security, ROLES.t1_analyst); + await createUserAndRole(getService, ROLES.t1_analyst); const { body } = await supertestWithoutAuth .delete(DETECTION_ENGINE_SIGNALS_MIGRATION_URL) @@ -119,7 +117,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(deletedMigration.id).to.eql(createdMigration.migration_id); expect(deletedMigration.error).to.eql({ message: - 'security_exception: action [indices:admin/settings/update] is unauthorized for user [t1_analyst] on indices [], this action is granted by the privileges [manage,all]', + 'security_exception: action [indices:admin/settings/update] is unauthorized for user [t1_analyst] on indices [], this action is granted by the index privileges [manage,all]', status_code: 403, }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts index 64c0c6666469a..0fd05904d5e33 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts @@ -21,7 +21,7 @@ import { getIndexNameFromLoad, waitFor, } from '../../utils'; -import { createUserAndRole } from '../roles_users_utils'; +import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils'; interface StatusResponse { index: string; @@ -44,12 +44,10 @@ interface FinalizeResponse { export default ({ getService }: FtrProviderContext): void => { const esArchiver = getService('esArchiver'); const kbnClient = getService('kibanaServer'); - const security = getService('security'); const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/88302 - describe.skip('Finalizing signals migrations', () => { + describe('Finalizing signals migrations', () => { let legacySignalsIndexName: string; let outdatedSignalsIndexName: string; let createdMigrations: CreateResponse[]; @@ -234,7 +232,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('rejects the request if the user does not have sufficient privileges', async () => { - await createUserAndRole(security, ROLES.t1_analyst); + await createUserAndRole(getService, ROLES.t1_analyst); const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL) @@ -249,9 +247,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(finalizeResponse.completed).not.to.eql(true); expect(finalizeResponse.error).to.eql({ message: - 'security_exception: action [cluster:monitor/task/get] is unauthorized for user [t1_analyst]', + 'security_exception: action [cluster:monitor/task/get] is unauthorized for user [t1_analyst], this action is granted by the cluster privileges [monitor,manage,all]', status_code: 403, }); + + await deleteUserAndRole(getService, ROLES.t1_analyst); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts index 3b9ec8e0909dc..793dec9eaae4b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts @@ -11,12 +11,11 @@ import { DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL } from '../../../../plugi import { ROLES } from '../../../../plugins/security_solution/common/test'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, deleteSignalsIndex, getIndexNameFromLoad } from '../../utils'; -import { createUserAndRole } from '../roles_users_utils'; +import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const esArchiver = getService('esArchiver'); - const security = getService('security'); const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -99,7 +98,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('rejects the request if the user does not have sufficient privileges', async () => { - await createUserAndRole(security, ROLES.t1_analyst); + await createUserAndRole(getService, ROLES.t1_analyst); await supertestWithoutAuth .get(DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL) @@ -107,6 +106,8 @@ export default ({ getService }: FtrProviderContext): void => { .auth(ROLES.t1_analyst, 'changeme') .query({ from: '2020-10-10' }) .expect(403); + + await deleteUserAndRole(getService, ROLES.t1_analyst); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts index 973b643a7a425..36a05f0ae8c0e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts @@ -27,7 +27,7 @@ import { waitForRuleSuccessOrStatus, getRuleForSignalTesting, } from '../../utils'; -import { createUserAndRole } from '../roles_users_utils'; +import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils'; import { ROLES } from '../../../../plugins/security_solution/common/test'; // eslint-disable-next-line import/no-default-export @@ -35,7 +35,6 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - const securityService = getService('security'); describe('open_close_signals', () => { describe('validation checks', () => { @@ -172,7 +171,7 @@ export default ({ getService }: FtrProviderContext) => { const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); - await createUserAndRole(securityService, ROLES.t1_analyst); + await createUserAndRole(getService, ROLES.t1_analyst); const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); @@ -203,6 +202,8 @@ export default ({ getService }: FtrProviderContext) => { }) => status === 'closed' ); expect(everySignalOpen).to.eql(true); + + await deleteUserAndRole(getService, ROLES.t1_analyst); }); it('should be able to close signals with soc_manager user', async () => { @@ -211,7 +212,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const userAndRole = ROLES.soc_manager; - await createUserAndRole(securityService, userAndRole); + await createUserAndRole(getService, userAndRole); const signalsOpen = await getSignalsByIds(supertest, [id]); const signalIds = signalsOpen.hits.hits.map((signal) => signal._id); @@ -240,6 +241,8 @@ export default ({ getService }: FtrProviderContext) => { }) => status === 'closed' ); expect(everySignalClosed).to.eql(true); + + await deleteUserAndRole(getService, userAndRole); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts index 614f08295b38f..f8949daea831e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts @@ -10,14 +10,14 @@ import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../plugins/security_so import { FtrProviderContext } from '../../common/ftr_provider_context'; import { ROLES } from '../../../../plugins/security_solution/common/test'; +import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/90229 - describe.skip('read_privileges', () => { + describe('read_privileges', () => { it('should return expected privileges for elastic admin', async () => { const { body } = await supertest.get(DETECTION_ENGINE_PRIVILEGES_URL).send().expect(200); expect(body).to.eql({ @@ -78,6 +78,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return expected privileges for a "reader" user', async () => { + await createUserAndRole(getService, ROLES.reader); const { body } = await supertestWithoutAuth .get(DETECTION_ENGINE_PRIVILEGES_URL) .auth(ROLES.reader, 'changeme') @@ -138,9 +139,11 @@ export default ({ getService }: FtrProviderContext) => { is_authenticated: true, has_encryption_key: true, }); + await deleteUserAndRole(getService, ROLES.reader); }); it('should return expected privileges for a "t1_analyst" user', async () => { + await createUserAndRole(getService, ROLES.t1_analyst); const { body } = await supertestWithoutAuth .get(DETECTION_ENGINE_PRIVILEGES_URL) .auth(ROLES.t1_analyst, 'changeme') @@ -201,9 +204,11 @@ export default ({ getService }: FtrProviderContext) => { is_authenticated: true, has_encryption_key: true, }); + await deleteUserAndRole(getService, ROLES.t1_analyst); }); it('should return expected privileges for a "t2_analyst" user', async () => { + await createUserAndRole(getService, ROLES.t2_analyst); const { body } = await supertestWithoutAuth .get(DETECTION_ENGINE_PRIVILEGES_URL) .auth(ROLES.t2_analyst, 'changeme') @@ -264,9 +269,11 @@ export default ({ getService }: FtrProviderContext) => { is_authenticated: true, has_encryption_key: true, }); + await deleteUserAndRole(getService, ROLES.t2_analyst); }); it('should return expected privileges for a "hunter" user', async () => { + await createUserAndRole(getService, ROLES.hunter); const { body } = await supertestWithoutAuth .get(DETECTION_ENGINE_PRIVILEGES_URL) .auth(ROLES.hunter, 'changeme') @@ -327,9 +334,11 @@ export default ({ getService }: FtrProviderContext) => { is_authenticated: true, has_encryption_key: true, }); + await deleteUserAndRole(getService, ROLES.hunter); }); it('should return expected privileges for a "rule_author" user', async () => { + await createUserAndRole(getService, ROLES.rule_author); const { body } = await supertestWithoutAuth .get(DETECTION_ENGINE_PRIVILEGES_URL) .auth(ROLES.rule_author, 'changeme') @@ -390,9 +399,11 @@ export default ({ getService }: FtrProviderContext) => { is_authenticated: true, has_encryption_key: true, }); + await deleteUserAndRole(getService, ROLES.rule_author); }); it('should return expected privileges for a "soc_manager" user', async () => { + await createUserAndRole(getService, ROLES.soc_manager); const { body } = await supertestWithoutAuth .get(DETECTION_ENGINE_PRIVILEGES_URL) .auth(ROLES.soc_manager, 'changeme') @@ -453,9 +464,11 @@ export default ({ getService }: FtrProviderContext) => { is_authenticated: true, has_encryption_key: true, }); + await deleteUserAndRole(getService, ROLES.soc_manager); }); it('should return expected privileges for a "platform_engineer" user', async () => { + await createUserAndRole(getService, ROLES.platform_engineer); const { body } = await supertestWithoutAuth .get(DETECTION_ENGINE_PRIVILEGES_URL) .auth(ROLES.platform_engineer, 'changeme') @@ -516,9 +529,11 @@ export default ({ getService }: FtrProviderContext) => { is_authenticated: true, has_encryption_key: true, }); + await deleteUserAndRole(getService, ROLES.platform_engineer); }); it('should return expected privileges for a "detections_admin" user', async () => { + await createUserAndRole(getService, ROLES.detections_admin); const { body } = await supertestWithoutAuth .get(DETECTION_ENGINE_PRIVILEGES_URL) .auth(ROLES.detections_admin, 'changeme') @@ -579,6 +594,7 @@ export default ({ getService }: FtrProviderContext) => { is_authenticated: true, has_encryption_key: true, }); + await deleteUserAndRole(getService, ROLES.detections_admin); }); }); }; From 95948fd5ebdeeb6f7dd6177df6f2d104b56f10e0 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Tue, 9 Feb 2021 15:12:13 -0600 Subject: [PATCH 075/100] [Metrics UI] Add warning severity to Metric Alerts (#90070) --- .../infra/common/alerting/metrics/index.ts | 4 +- .../infra/common/alerting/metrics/types.ts | 17 +- .../common/components/alert_preview.tsx | 208 ++++++++++++---- .../inventory/components/expression.tsx | 184 ++++++++++++-- .../inventory/components/validation.tsx | 80 +++++-- .../components/expression_chart.tsx | 226 ++++++++++-------- .../components/expression_row.tsx | 203 +++++++++++++--- .../components/validation.tsx | 74 ++++-- .../server/lib/alerting/common/messages.ts | 3 + .../infra/server/lib/alerting/common/types.ts | 9 + .../evaluate_condition.ts | 25 +- .../inventory_metric_threshold_executor.ts | 38 ++- ...review_inventory_metric_threshold_alert.ts | 22 +- ...r_inventory_metric_threshold_alert_type.ts | 20 +- .../inventory_metric_threshold/types.ts | 2 + .../metric_threshold/lib/evaluate_alert.ts | 21 +- .../metric_threshold_executor.ts | 50 +++- .../preview_metric_threshold_alert.test.ts | 66 ++--- .../preview_metric_threshold_alert.ts | 25 +- .../register_metric_threshold_alert_type.ts | 14 +- .../lib/alerting/metric_threshold/types.ts | 2 + .../infra/server/routes/alerting/preview.ts | 73 +++--- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 7 +- 24 files changed, 998 insertions(+), 379 deletions(-) diff --git a/x-pack/plugins/infra/common/alerting/metrics/index.ts b/x-pack/plugins/infra/common/alerting/metrics/index.ts index 5151a40c7e8b1..2c66638711cd0 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/index.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/index.ts @@ -10,8 +10,8 @@ export const INFRA_ALERT_PREVIEW_PATH = '/api/infra/alerting/preview'; export const TOO_MANY_BUCKETS_PREVIEW_EXCEPTION = 'TOO_MANY_BUCKETS_PREVIEW_EXCEPTION'; export interface TooManyBucketsPreviewExceptionMetadata { - TOO_MANY_BUCKETS_PREVIEW_EXCEPTION: any; - maxBuckets: number; + TOO_MANY_BUCKETS_PREVIEW_EXCEPTION: boolean; + maxBuckets: any; } export const isTooManyBucketsPreviewException = ( value: any diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 47a5202cc7275..a89f82e931fd4 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -90,12 +90,17 @@ export type AlertPreviewRequestParams = rt.TypeOf = (props) => { return unthrottledNotifications > notifications; }, [previewResult, showNoDataResults]); + const hasWarningThreshold = useMemo( + () => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')), + [alertParams] + ); + return ( = (props) => { - - - - ), - }} - />{' '} - {previewResult.groupByDisplayName ? ( - <> - {' '} - - - {' '} - - ) : null} - e.value === previewResult.previewLookbackInterval - )?.shortText, - }} - /> - + } > {showNoDataResults && previewResult.resultTotals.noData ? ( + ), boldedResultsNumber: ( {i18n.translate( 'xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber', { - defaultMessage: - '{noData, plural, one {was # result} other {were # results}}', + defaultMessage: '{noData, plural, one {# result} other {# results}}', values: { noData: previewResult.resultTotals.noData, }, @@ -361,6 +332,145 @@ export const AlertPreview: React.FC = (props) => { ); }; +const PreviewTextString = ({ + previewResult, + hasWarningThreshold, +}: { + previewResult: AlertPreviewSuccessResponsePayload & Record; + hasWarningThreshold: boolean; +}) => { + const instanceCount = hasWarningThreshold ? ( + + ), + criticalInstances: ( + + + + ), + warningInstances: ( + + + + ), + boldCritical: ( + + + + ), + boldWarning: ( + + + + ), + }} + /> + ) : ( + + ), + firedTimes: ( + + + + ), + }} + /> + ); + + const groupByText = previewResult.groupByDisplayName ? ( + <> + + + + ), + }} + />{' '} + + ) : ( + <> + ); + + const lookbackText = ( + e.value === previewResult.previewLookbackInterval) + ?.shortText, + }} + /> + ); + + return ( + + ); +}; + const previewOptions = [ { value: 'h', diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index d403c254f2bd0..4a05521e9fc87 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { debounce, pick } from 'lodash'; +import { debounce, pick, omit } from 'lodash'; import { Unit } from '@elastic/datemath'; import React, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react'; import { IFieldType } from 'src/plugins/data/public'; @@ -21,6 +21,7 @@ import { EuiCheckbox, EuiToolTip, EuiIcon, + EuiHealth, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -423,9 +424,24 @@ const StyledExpression = euiStyled.div` padding: 0 4px; `; +const StyledHealth = euiStyled(EuiHealth)` + margin-left: 4px; +`; + export const ExpressionRow: React.FC = (props) => { const { setAlertParams, expression, errors, expressionId, remove, canDelete, fields } = props; - const { metric, comparator = Comparator.GT, threshold = [], customMetric } = expression; + const { + metric, + comparator = Comparator.GT, + threshold = [], + customMetric, + warningThreshold = [], + warningComparator, + } = expression; + + const [displayWarningThreshold, setDisplayWarningThreshold] = useState( + Boolean(warningThreshold?.length) + ); const updateMetric = useCallback( (m?: SnapshotMetricType | string) => { @@ -452,6 +468,13 @@ export const ExpressionRow: React.FC = (props) => { [expressionId, expression, setAlertParams] ); + const updateWarningComparator = useCallback( + (c?: string) => { + setAlertParams(expressionId, { ...expression, warningComparator: c as Comparator }); + }, + [expressionId, expression, setAlertParams] + ); + const updateThreshold = useCallback( (t) => { if (t.join() !== expression.threshold.join()) { @@ -461,6 +484,58 @@ export const ExpressionRow: React.FC = (props) => { [expressionId, expression, setAlertParams] ); + const updateWarningThreshold = useCallback( + (t) => { + if (t.join() !== expression.warningThreshold?.join()) { + setAlertParams(expressionId, { ...expression, warningThreshold: t }); + } + }, + [expressionId, expression, setAlertParams] + ); + + const toggleWarningThreshold = useCallback(() => { + if (!displayWarningThreshold) { + setDisplayWarningThreshold(true); + setAlertParams(expressionId, { + ...expression, + warningComparator: comparator, + warningThreshold: [], + }); + } else { + setDisplayWarningThreshold(false); + setAlertParams(expressionId, omit(expression, 'warningComparator', 'warningThreshold')); + } + }, [ + displayWarningThreshold, + setDisplayWarningThreshold, + setAlertParams, + comparator, + expression, + expressionId, + ]); + + const criticalThresholdExpression = ( + + ); + + const warningThresholdExpression = displayWarningThreshold && ( + + ); + const ofFields = useMemo(() => { let myMetrics = hostMetricTypes; @@ -515,25 +590,62 @@ export const ExpressionRow: React.FC = (props) => { fields={fields} /> - - - - {metric && ( -
    - {metricUnit[metric]?.label || ''} -
    - )} + {!displayWarningThreshold && criticalThresholdExpression} + {displayWarningThreshold && ( + <> + + {criticalThresholdExpression} + + + + + + {warningThresholdExpression} + + + + + + + )} + {!displayWarningThreshold && ( + <> + {' '} + + + + + + + + )} {canDelete && ( @@ -553,6 +665,38 @@ export const ExpressionRow: React.FC = (props) => { ); }; +const ThresholdElement: React.FC<{ + updateComparator: (c?: string) => void; + updateThreshold: (t?: number[]) => void; + threshold: InventoryMetricConditions['threshold']; + comparator: InventoryMetricConditions['comparator']; + errors: IErrorObject; + metric?: SnapshotMetricType; +}> = ({ updateComparator, updateThreshold, threshold, metric, comparator, errors }) => { + return ( + <> + + + + {metric && ( +
    + {metricUnit[metric]?.label || ''} +
    + )} + + ); +}; + const getDisplayNameForType = (type: InventoryItemType) => { const inventoryModel = findInventoryModel(type); return inventoryModel.displayName; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx index 7a87998c8ce1f..c8bab76d79a4e 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/validation.tsx @@ -6,8 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; +import { + InventoryMetricConditions, + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/inventory_metric_threshold/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; @@ -21,8 +24,14 @@ export function validateMetricThreshold({ [id: string]: { timeSizeUnit: string[]; timeWindowSize: string[]; - threshold0: string[]; - threshold1: string[]; + critical: { + threshold0: string[]; + threshold1: string[]; + }; + warning: { + threshold0: string[]; + threshold1: string[]; + }; metric: string[]; }; } = {}; @@ -39,41 +48,64 @@ export function validateMetricThreshold({ errors[id] = errors[id] || { timeSizeUnit: [], timeWindowSize: [], - threshold0: [], - threshold1: [], + critical: { + threshold0: [], + threshold1: [], + }, + warning: { + threshold0: [], + threshold1: [], + }, metric: [], }; if (!c.threshold || !c.threshold.length) { - errors[id].threshold0.push( + errors[id].critical.threshold0.push( i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { defaultMessage: 'Threshold is required.', }) ); } - // The Threshold component returns an empty array with a length ([empty]) because it's using delete newThreshold[i]. - // We need to use [...c.threshold] to convert it to an array with an undefined value ([undefined]) so we can test each element. - if (c.threshold && c.threshold.length && ![...c.threshold].every(isNumber)) { - [...c.threshold].forEach((v, i) => { - if (!isNumber(v)) { - const key = i === 0 ? 'threshold0' : 'threshold1'; - errors[id][key].push( - i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired', { - defaultMessage: 'Thresholds must contain a valid number.', - }) - ); - } - }); - } - - if (c.comparator === 'between' && (!c.threshold || c.threshold.length < 2)) { - errors[id].threshold1.push( + if (c.warningThreshold && !c.warningThreshold.length) { + errors[id].warning.threshold0.push( i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { defaultMessage: 'Threshold is required.', }) ); } + for (const props of [ + { comparator: c.comparator, threshold: c.threshold, type: 'critical' }, + { comparator: c.warningComparator, threshold: c.warningThreshold, type: 'warning' }, + ]) { + // The Threshold component returns an empty array with a length ([empty]) because it's using delete newThreshold[i]. + // We need to use [...c.threshold] to convert it to an array with an undefined value ([undefined]) so we can test each element. + const { comparator, threshold, type } = props as { + comparator?: Comparator; + threshold?: number[]; + type: 'critical' | 'warning'; + }; + if (threshold && threshold.length && ![...threshold].every(isNumber)) { + [...threshold].forEach((v, i) => { + if (!isNumber(v)) { + const key = i === 0 ? 'threshold0' : 'threshold1'; + errors[id][type][key].push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired', { + defaultMessage: 'Thresholds must contain a valid number.', + }) + ); + } + }); + } + + if (comparator === Comparator.BETWEEN && (!threshold || threshold.length < 2)) { + errors[id][type].threshold1.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Threshold is required.', + }) + ); + } + } if (!c.timeSize) { errors[id].timeWindowSize.push( diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 65842089863f3..c98984b5475cd 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -111,7 +111,9 @@ export const ExpressionChart: React.FC = ({ ); } - const thresholds = expression.threshold.slice().sort(); + const criticalThresholds = expression.threshold.slice().sort(); + const warningThresholds = expression.warningThreshold?.slice().sort() ?? []; + const thresholds = [...criticalThresholds, ...warningThresholds].sort(); // Creating a custom series where the ID is changed to 0 // so that we can get a proper domian @@ -145,108 +147,70 @@ export const ExpressionChart: React.FC = ({ const dataDomain = calculateDomain(series, [metric], false); const domain = { max: Math.max(dataDomain.max, last(thresholds) || dataDomain.max) * 1.1, // add 10% headroom. - min: Math.min(dataDomain.min, first(thresholds) || dataDomain.min), + min: Math.min(dataDomain.min, first(thresholds) || dataDomain.min) * 0.9, // add 10% floor, }; if (domain.min === first(expression.threshold)) { domain.min = domain.min * 0.9; } - const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(expression.comparator); - const isBelow = [Comparator.LT, Comparator.LT_OR_EQ].includes(expression.comparator); const opacity = 0.3; const { timeSize, timeUnit } = expression; const timeLabel = TIME_LABELS[timeUnit as keyof typeof TIME_LABELS]; - return ( - <> - - - - ({ - dataValue: threshold, - }))} - style={{ - line: { - strokeWidth: 2, - stroke: colorTransformer(Color.color1), - opacity: 1, - }, - }} - /> - {thresholds.length === 2 && expression.comparator === Comparator.BETWEEN ? ( - <> - - - ) : null} - {thresholds.length === 2 && expression.comparator === Comparator.OUTSIDE_RANGE ? ( - <> - - & { sortedThresholds: number[]; color: Color; id: string }) => { + if (!comparator || !threshold) return null; + const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(comparator); + const isBelow = [Comparator.LT, Comparator.LT_OR_EQ].includes(comparator); + return ( + <> + ({ + dataValue: t, + }))} + style={{ + line: { + strokeWidth: 2, + stroke: colorTransformer(color), + opacity: 1, + }, + }} + /> + {sortedThresholds.length === 2 && comparator === Comparator.BETWEEN ? ( + <> + - - ) : null} - {isBelow && first(expression.threshold) != null ? ( + }, + ]} + /> + + ) : null} + {sortedThresholds.length === 2 && comparator === Comparator.OUTSIDE_RANGE ? ( + <> = ({ x0: firstTimestamp, x1: lastTimestamp, y0: domain.min, - y1: first(expression.threshold), + y1: first(threshold), }, }, ]} /> - ) : null} - {isAbove && first(expression.threshold) != null ? ( = ({ coordinates: { x0: firstTimestamp, x1: lastTimestamp, - y0: first(expression.threshold), + y0: last(threshold), y1: domain.max, }, }, ]} /> - ) : null} + + ) : null} + {isBelow && first(threshold) != null ? ( + + ) : null} + {isAbove && first(threshold) != null ? ( + + ) : null} + + ); + }; + + return ( + <> + + + + + {expression.warningComparator && expression.warningThreshold && ( + + )} = (props) => { const [isExpanded, setRowState] = useState(true); const toggleRowState = useCallback(() => setRowState(!isExpanded), [isExpanded]); @@ -85,9 +92,14 @@ export const ExpressionRow: React.FC = (props) => { metric, comparator = Comparator.GT, threshold = [], + warningThreshold = [], + warningComparator, } = expression; + const [displayWarningThreshold, setDisplayWarningThreshold] = useState( + Boolean(warningThreshold?.length) + ); - const isMetricPct = useMemo(() => metric && metric.endsWith('.pct'), [metric]); + const isMetricPct = useMemo(() => Boolean(metric && metric.endsWith('.pct')), [metric]); const updateAggType = useCallback( (at: string) => { @@ -114,22 +126,81 @@ export const ExpressionRow: React.FC = (props) => { [expressionId, expression, setAlertParams] ); + const updateWarningComparator = useCallback( + (c?: string) => { + setAlertParams(expressionId, { ...expression, warningComparator: c as Comparator }); + }, + [expressionId, expression, setAlertParams] + ); + + const convertThreshold = useCallback( + (enteredThreshold) => + isMetricPct ? enteredThreshold.map((v: number) => pctToDecimal(v)) : enteredThreshold, + [isMetricPct] + ); + const updateThreshold = useCallback( (enteredThreshold) => { - const t = isMetricPct - ? enteredThreshold.map((v: number) => pctToDecimal(v)) - : enteredThreshold; + const t = convertThreshold(enteredThreshold); if (t.join() !== expression.threshold.join()) { setAlertParams(expressionId, { ...expression, threshold: t }); } }, - [expressionId, expression, isMetricPct, setAlertParams] + [expressionId, expression, convertThreshold, setAlertParams] ); - const displayedThreshold = useMemo(() => { - if (isMetricPct) return threshold.map((v) => decimalToPct(v)); - return threshold; - }, [threshold, isMetricPct]); + const updateWarningThreshold = useCallback( + (enteredThreshold) => { + const t = convertThreshold(enteredThreshold); + if (t.join() !== expression.warningThreshold?.join()) { + setAlertParams(expressionId, { ...expression, warningThreshold: t }); + } + }, + [expressionId, expression, convertThreshold, setAlertParams] + ); + + const toggleWarningThreshold = useCallback(() => { + if (!displayWarningThreshold) { + setDisplayWarningThreshold(true); + setAlertParams(expressionId, { + ...expression, + warningComparator: comparator, + warningThreshold: [], + }); + } else { + setDisplayWarningThreshold(false); + setAlertParams(expressionId, omit(expression, 'warningComparator', 'warningThreshold')); + } + }, [ + displayWarningThreshold, + setDisplayWarningThreshold, + setAlertParams, + comparator, + expression, + expressionId, + ]); + + const criticalThresholdExpression = ( + + ); + + const warningThresholdExpression = displayWarningThreshold && ( + + ); return ( <> @@ -187,26 +258,62 @@ export const ExpressionRow: React.FC = (props) => { /> )} - - - - {isMetricPct && ( -
    - % -
    - )} + {!displayWarningThreshold && criticalThresholdExpression} + {displayWarningThreshold && ( + <> + + {criticalThresholdExpression} + + + + + + {warningThresholdExpression} + + + + + + + )} + {!displayWarningThreshold && ( + <> + {' '} + + + + + + + + )}
    {canDelete && ( @@ -227,6 +334,44 @@ export const ExpressionRow: React.FC = (props) => { ); }; +const ThresholdElement: React.FC<{ + updateComparator: (c?: string) => void; + updateThreshold: (t?: number[]) => void; + threshold: MetricExpression['threshold']; + isMetricPct: boolean; + comparator: MetricExpression['comparator']; + errors: IErrorObject; +}> = ({ updateComparator, updateThreshold, threshold, isMetricPct, comparator, errors }) => { + const displayedThreshold = useMemo(() => { + if (isMetricPct) return threshold.map((v) => decimalToPct(v)); + return threshold; + }, [threshold, isMetricPct]); + + return ( + <> + + + + {isMetricPct && ( +
    + % +
    + )} + + ); +}; + export const aggregationType: { [key: string]: any } = { avg: { text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx index bab396df9da0d..69b2f1d1bcc8f 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx @@ -25,8 +25,14 @@ export function validateMetricThreshold({ aggField: string[]; timeSizeUnit: string[]; timeWindowSize: string[]; - threshold0: string[]; - threshold1: string[]; + critical: { + threshold0: string[]; + threshold1: string[]; + }; + warning: { + threshold0: string[]; + threshold1: string[]; + }; metric: string[]; }; } = {}; @@ -44,8 +50,14 @@ export function validateMetricThreshold({ aggField: [], timeSizeUnit: [], timeWindowSize: [], - threshold0: [], - threshold1: [], + critical: { + threshold0: [], + threshold1: [], + }, + warning: { + threshold0: [], + threshold1: [], + }, metric: [], }; if (!c.aggType) { @@ -57,36 +69,54 @@ export function validateMetricThreshold({ } if (!c.threshold || !c.threshold.length) { - errors[id].threshold0.push( + errors[id].critical.threshold0.push( i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { defaultMessage: 'Threshold is required.', }) ); } - // The Threshold component returns an empty array with a length ([empty]) because it's using delete newThreshold[i]. - // We need to use [...c.threshold] to convert it to an array with an undefined value ([undefined]) so we can test each element. - if (c.threshold && c.threshold.length && ![...c.threshold].every(isNumber)) { - [...c.threshold].forEach((v, i) => { - if (!isNumber(v)) { - const key = i === 0 ? 'threshold0' : 'threshold1'; - errors[id][key].push( - i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired', { - defaultMessage: 'Thresholds must contain a valid number.', - }) - ); - } - }); - } - - if (c.comparator === Comparator.BETWEEN && (!c.threshold || c.threshold.length < 2)) { - errors[id].threshold1.push( + if (c.warningThreshold && !c.warningThreshold.length) { + errors[id].warning.threshold0.push( i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { defaultMessage: 'Threshold is required.', }) ); } + for (const props of [ + { comparator: c.comparator, threshold: c.threshold, type: 'critical' }, + { comparator: c.warningComparator, threshold: c.warningThreshold, type: 'warning' }, + ]) { + // The Threshold component returns an empty array with a length ([empty]) because it's using delete newThreshold[i]. + // We need to use [...c.threshold] to convert it to an array with an undefined value ([undefined]) so we can test each element. + const { comparator, threshold, type } = props as { + comparator?: Comparator; + threshold?: number[]; + type: 'critical' | 'warning'; + }; + if (threshold && threshold.length && ![...threshold].every(isNumber)) { + [...threshold].forEach((v, i) => { + if (!isNumber(v)) { + const key = i === 0 ? 'threshold0' : 'threshold1'; + errors[id][type][key].push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired', { + defaultMessage: 'Thresholds must contain a valid number.', + }) + ); + } + }); + } + + if (comparator === Comparator.BETWEEN && (!threshold || threshold.length < 2)) { + errors[id][type].threshold1.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Threshold is required.', + }) + ); + } + } + if (!c.timeSize) { errors[id].timeWindowSize.push( i18n.translate('xpack.infra.metrics.alertFlyout.error.timeRequred', { diff --git a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts index 9f0be1679448f..b692629209849 100644 --- a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts +++ b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts @@ -19,6 +19,9 @@ export const stateToAlertMessage = { [AlertStates.ALERT]: i18n.translate('xpack.infra.metrics.alerting.threshold.alertState', { defaultMessage: 'ALERT', }), + [AlertStates.WARNING]: i18n.translate('xpack.infra.metrics.alerting.threshold.warningState', { + defaultMessage: 'WARNING', + }), [AlertStates.NO_DATA]: i18n.translate('xpack.infra.metrics.alerting.threshold.noDataState', { defaultMessage: 'NO DATA', }), diff --git a/x-pack/plugins/infra/server/lib/alerting/common/types.ts b/x-pack/plugins/infra/server/lib/alerting/common/types.ts index e4db2600e316d..0b809429de0d2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/common/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/common/types.ts @@ -29,6 +29,15 @@ export enum Aggregators { export enum AlertStates { OK, ALERT, + WARNING, NO_DATA, ERROR, } + +export interface PreviewResult { + fired: number; + warning: number; + noData: number; + error: number; + notifications: number; +} diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 16f74d579969a..ea37f7adda7c4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -26,6 +26,7 @@ import { getNodes } from '../../../routes/snapshot/lib/get_nodes'; type ConditionResult = InventoryMetricConditions & { shouldFire: boolean[]; + shouldWarn: boolean[]; currentValue: number; isNoData: boolean[]; isError: boolean; @@ -39,8 +40,8 @@ export const evaluateCondition = async ( filterQuery?: string, lookbackSize?: number ): Promise> => { - const { comparator, metric, customMetric } = condition; - let { threshold } = condition; + const { comparator, warningComparator, metric, customMetric } = condition; + let { threshold, warningThreshold } = condition; const timerange = { to: Date.now(), @@ -62,19 +63,22 @@ export const evaluateCondition = async ( ); threshold = threshold.map((n) => convertMetricValue(metric, n)); - - const comparisonFunction = comparatorMap[comparator]; + warningThreshold = warningThreshold?.map((n) => convertMetricValue(metric, n)); + + const valueEvaluator = (value?: DataValue, t?: number[], c?: Comparator) => { + if (value === undefined || value === null || !t || !c) return [false]; + const comparisonFunction = comparatorMap[c]; + return Array.isArray(value) + ? value.map((v) => comparisonFunction(Number(v), t)) + : [comparisonFunction(value as number, t)]; + }; const result = mapValues(currentValues, (value) => { if (isTooManyBucketsPreviewException(value)) throw value; return { ...condition, - shouldFire: - value !== undefined && - value !== null && - (Array.isArray(value) - ? value.map((v) => comparisonFunction(Number(v), threshold)) - : [comparisonFunction(value as number, threshold)]), + shouldFire: valueEvaluator(value, threshold, comparator), + shouldWarn: valueEvaluator(value, warningThreshold, warningComparator), isNoData: Array.isArray(value) ? value.map((v) => v === null) : [value === null], isError: value === undefined, currentValue: getCurrentValue(value), @@ -90,6 +94,7 @@ const getCurrentValue: (value: any) => number = (value) => { return NaN; }; +type DataValue = number | null | Array; const getData = async ( callCluster: AlertServices['callCluster'], nodeType: InventoryItemType, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 2658fa6820274..a15f1010194a5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -81,6 +81,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = // Grab the result of the most recent bucket last(result[item].shouldFire) ); + const shouldAlertWarn = results.every((result) => last(result[item].shouldWarn)); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state @@ -93,12 +94,20 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = ? AlertStates.NO_DATA : shouldAlertFire ? AlertStates.ALERT + : shouldAlertWarn + ? AlertStates.WARNING : AlertStates.OK; let reason; - if (nextState === AlertStates.ALERT) { + if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) { reason = results - .map((result) => buildReasonWithVerboseMetricName(result[item], buildFiredAlertReason)) + .map((result) => + buildReasonWithVerboseMetricName( + result[item], + buildFiredAlertReason, + nextState === AlertStates.WARNING + ) + ) .join('\n'); } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { /* @@ -125,7 +134,11 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } if (reason) { const actionGroupId = - nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS_ID; + nextState === AlertStates.OK + ? RecoveredActionGroup.id + : nextState === AlertStates.WARNING + ? WARNING_ACTIONS.id + : FIRED_ACTIONS.id; alertInstance.scheduleActions( /** * TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on @@ -152,7 +165,11 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } }; -const buildReasonWithVerboseMetricName = (resultItem: any, buildReason: (r: any) => string) => { +const buildReasonWithVerboseMetricName = ( + resultItem: any, + buildReason: (r: any) => string, + useWarningThreshold?: boolean +) => { if (!resultItem) return ''; const resultWithVerboseMetricName = { ...resultItem, @@ -162,6 +179,8 @@ const buildReasonWithVerboseMetricName = (resultItem: any, buildReason: (r: any) ? getCustomMetricLabel(resultItem.customMetric) : resultItem.metric), currentValue: formatMetric(resultItem.metric, resultItem.currentValue), + threshold: useWarningThreshold ? resultItem.warningThreshold! : resultItem.threshold, + comparator: useWarningThreshold ? resultItem.warningComparator! : resultItem.comparator, }; return buildReason(resultWithVerboseMetricName); }; @@ -177,11 +196,18 @@ const mapToConditionsLookup = ( {} ); -export const FIRED_ACTIONS_ID = 'metrics.invenotry_threshold.fired'; +export const FIRED_ACTIONS_ID = 'metrics.inventory_threshold.fired'; export const FIRED_ACTIONS: ActionGroup = { id: FIRED_ACTIONS_ID, name: i18n.translate('xpack.infra.metrics.alerting.inventory.threshold.fired', { - defaultMessage: 'Fired', + defaultMessage: 'Alert', + }), +}; +export const WARNING_ACTIONS_ID = 'metrics.inventory_threshold.warning'; +export const WARNING_ACTIONS = { + id: WARNING_ACTIONS_ID, + name: i18n.translate('xpack.infra.metrics.alerting.threshold.warning', { + defaultMessage: 'Warning', }), }; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index 528c0f92d20e7..5fff76260e5c6 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -7,6 +7,7 @@ import { Unit } from '@elastic/datemath'; import { first } from 'lodash'; +import { PreviewResult } from '../common/types'; import { InventoryMetricConditions } from './types'; import { TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, @@ -35,7 +36,9 @@ interface PreviewInventoryMetricThresholdAlertParams { alertOnNoData: boolean; } -export const previewInventoryMetricThresholdAlert = async ({ +export const previewInventoryMetricThresholdAlert: ( + params: PreviewInventoryMetricThresholdAlertParams +) => Promise = async ({ callCluster, params, source, @@ -43,7 +46,7 @@ export const previewInventoryMetricThresholdAlert = async ({ alertInterval, alertThrottle, alertOnNoData, -}: PreviewInventoryMetricThresholdAlertParams) => { +}) => { const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); @@ -74,6 +77,7 @@ export const previewInventoryMetricThresholdAlert = async ({ const numberOfResultBuckets = lookbackSize; const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution); let numberOfTimesFired = 0; + let numberOfTimesWarned = 0; let numberOfNoDataResults = 0; let numberOfErrors = 0; let numberOfNotifications = 0; @@ -88,6 +92,9 @@ export const previewInventoryMetricThresholdAlert = async ({ const shouldFire = result[item].shouldFire as boolean[]; return shouldFire[mappedBucketIndex]; }); + const allConditionsWarnInMappedBucket = + !allConditionsFiredInMappedBucket && + results.every((result) => result[item].shouldWarn[mappedBucketIndex]); const someConditionsNoDataInMappedBucket = results.some((result) => { const hasNoData = result[item].isNoData as boolean[]; return hasNoData[mappedBucketIndex]; @@ -108,6 +115,9 @@ export const previewInventoryMetricThresholdAlert = async ({ } else if (allConditionsFiredInMappedBucket) { numberOfTimesFired++; notifyWithThrottle(); + } else if (allConditionsWarnInMappedBucket) { + numberOfTimesWarned++; + notifyWithThrottle(); } else if (throttleTracker > 0) { throttleTracker++; } @@ -115,7 +125,13 @@ export const previewInventoryMetricThresholdAlert = async ({ throttleTracker = 0; } } - return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors, numberOfNotifications]; + return { + fired: numberOfTimesFired, + warning: numberOfTimesWarned, + noData: numberOfNoDataResults, + error: numberOfErrors, + notifications: numberOfNotifications, + }; }); return previewResults; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index 4ae1a0e4d5d49..6c439225d9d00 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -7,11 +7,17 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../../alerts/server'; +import { + AlertType, + AlertInstanceState, + AlertInstanceContext, + ActionGroupIdsOf, +} from '../../../../../alerts/server'; import { createInventoryMetricThresholdExecutor, FIRED_ACTIONS, FIRED_ACTIONS_ID, + WARNING_ACTIONS, } from './inventory_metric_threshold_executor'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; import { InfraBackendLibs } from '../../infra_types'; @@ -25,7 +31,6 @@ import { metricActionVariableDescription, thresholdActionVariableDescription, } from '../common/messages'; -import { RecoveredActionGroupId } from '../../../../../alerts/common'; const condition = schema.object({ threshold: schema.arrayOf(schema.number()), @@ -33,6 +38,8 @@ const condition = schema.object({ timeUnit: schema.string(), timeSize: schema.number(), metric: schema.string(), + warningThreshold: schema.maybe(schema.arrayOf(schema.number())), + warningComparator: schema.maybe(oneOfLiterals(Object.values(Comparator))), customMetric: schema.maybe( schema.object({ type: schema.literal('custom'), @@ -44,7 +51,9 @@ const condition = schema.object({ ), }); -export type InventoryMetricThresholdAllowedActionGroups = typeof FIRED_ACTIONS_ID; +export type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf< + typeof FIRED_ACTIONS | typeof WARNING_ACTIONS +>; export const registerMetricInventoryThresholdAlertType = ( libs: InfraBackendLibs @@ -56,8 +65,7 @@ export const registerMetricInventoryThresholdAlertType = ( Record, AlertInstanceState, AlertInstanceContext, - InventoryMetricThresholdAllowedActionGroups, - RecoveredActionGroupId + InventoryMetricThresholdAllowedActionGroups > => ({ id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.metrics.inventory.alertName', { @@ -78,7 +86,7 @@ export const registerMetricInventoryThresholdAlertType = ( ), }, defaultActionGroupId: FIRED_ACTIONS_ID, - actionGroups: [FIRED_ACTIONS], + actionGroups: [FIRED_ACTIONS, WARNING_ACTIONS], producer: 'infrastructure', minimumLicenseRequired: 'basic', executor: createInventoryMetricThresholdExecutor(libs), diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts index 28c41de9b10d6..120fa47c079ab 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts @@ -22,4 +22,6 @@ export interface InventoryMetricConditions { threshold: number[]; comparator: Comparator; customMetric?: SnapshotCustomMetricInput; + warningThreshold?: number[]; + warningComparator?: Comparator; } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index f9661e2cd56bb..029445a441eea 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -60,8 +60,17 @@ export const evaluateAlert = { + if (!t || !c) return [false]; + const comparisonFunction = comparatorMap[c]; + return Array.isArray(points) + ? points.map( + (point) => t && typeof point.value === 'number' && comparisonFunction(point.value, t) + ) + : [false]; + }; + return mapValues(currentValues, (points: any[] | typeof NaN | null) => { if (isTooManyBucketsPreviewException(points)) throw points; return { @@ -69,12 +78,8 @@ export const evaluateAlert = - typeof point.value === 'number' && comparisonFunction(point.value, threshold) - ) - : [false], + shouldFire: pointsEvaluator(points, threshold, comparator), + shouldWarn: pointsEvaluator(points, warningThreshold, warningComparator), isNoData: Array.isArray(points) ? points.map((point) => point?.value === null || point === null) : [points === null], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 17b9ab1cab907..b822d71b3f812 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -18,7 +18,7 @@ import { stateToAlertMessage, } from '../common/messages'; import { createFormatter } from '../../../../common/formatters'; -import { AlertStates } from './types'; +import { AlertStates, Comparator } from './types'; import { evaluateAlert, EvaluatedAlertParams } from './lib/evaluate_alert'; import { MetricThresholdAlertExecutorOptions, @@ -60,6 +60,7 @@ export const createMetricThresholdExecutor = ( // Grab the result of the most recent bucket last(result[group].shouldFire) ); + const shouldAlertWarn = alertResults.every((result) => last(result[group].shouldWarn)); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state const isNoData = alertResults.some((result) => last(result[group].isNoData)); @@ -71,12 +72,18 @@ export const createMetricThresholdExecutor = ( ? AlertStates.NO_DATA : shouldAlertFire ? AlertStates.ALERT + : shouldAlertWarn + ? AlertStates.WARNING : AlertStates.OK; let reason; - if (nextState === AlertStates.ALERT) { + if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) { reason = alertResults - .map((result) => buildFiredAlertReason(formatAlertResult(result[group]))) + .map((result) => + buildFiredAlertReason( + formatAlertResult(result[group], nextState === AlertStates.WARNING) + ) + ) .join('\n'); } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { /* @@ -105,7 +112,11 @@ export const createMetricThresholdExecutor = ( const firstResult = first(alertResults); const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString(); const actionGroupId = - nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; + nextState === AlertStates.OK + ? RecoveredActionGroup.id + : nextState === AlertStates.WARNING + ? WARNING_ACTIONS.id + : FIRED_ACTIONS.id; alertInstance.scheduleActions(actionGroupId, { group, alertState: stateToAlertMessage[nextState], @@ -132,7 +143,14 @@ export const createMetricThresholdExecutor = ( export const FIRED_ACTIONS = { id: 'metrics.threshold.fired', name: i18n.translate('xpack.infra.metrics.alerting.threshold.fired', { - defaultMessage: 'Fired', + defaultMessage: 'Alert', + }), +}; + +export const WARNING_ACTIONS = { + id: 'metrics.threshold.warning', + name: i18n.translate('xpack.infra.metrics.alerting.threshold.warning', { + defaultMessage: 'Warning', }), }; @@ -152,9 +170,20 @@ const formatAlertResult = ( metric: string; currentValue: number; threshold: number[]; - } & AlertResult + comparator: Comparator; + warningThreshold?: number[]; + warningComparator?: Comparator; + } & AlertResult, + useWarningThreshold?: boolean ) => { - const { metric, currentValue, threshold } = alertResult; + const { + metric, + currentValue, + threshold, + comparator, + warningThreshold, + warningComparator, + } = alertResult; const noDataValue = i18n.translate( 'xpack.infra.metrics.alerting.threshold.noDataFormattedValue', { @@ -167,12 +196,17 @@ const formatAlertResult = ( currentValue: currentValue ?? noDataValue, }; const formatter = createFormatter('percent'); + const thresholdToFormat = useWarningThreshold ? warningThreshold! : threshold; + const comparatorToFormat = useWarningThreshold ? warningComparator! : comparator; return { ...alertResult, currentValue: currentValue !== null && typeof currentValue !== 'undefined' ? formatter(currentValue) : noDataValue, - threshold: Array.isArray(threshold) ? threshold.map((v: number) => formatter(v)) : threshold, + threshold: Array.isArray(thresholdToFormat) + ? thresholdToFormat.map((v: number) => formatter(v)) + : thresholdToFormat, + comparator: comparatorToFormat, }; }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts index 8576fd7b59299..1adca25504b1f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts @@ -20,10 +20,10 @@ describe('Previewing the metric threshold alert type', () => { alertThrottle: '1m', alertOnNoData: true, }); - const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult; - expect(firedResults).toBe(30); - expect(noDataResults).toBe(0); - expect(errorResults).toBe(0); + const { fired, noData, error, notifications } = ungroupedResult; + expect(fired).toBe(30); + expect(noData).toBe(0); + expect(error).toBe(0); expect(notifications).toBe(30); }); @@ -35,10 +35,10 @@ describe('Previewing the metric threshold alert type', () => { alertThrottle: '3m', alertOnNoData: true, }); - const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult; - expect(firedResults).toBe(10); - expect(noDataResults).toBe(0); - expect(errorResults).toBe(0); + const { fired, noData, error, notifications } = ungroupedResult; + expect(fired).toBe(10); + expect(noData).toBe(0); + expect(error).toBe(0); expect(notifications).toBe(10); }); test('returns the expected results using a bucket interval longer than the alert interval', async () => { @@ -49,10 +49,10 @@ describe('Previewing the metric threshold alert type', () => { alertThrottle: '30s', alertOnNoData: true, }); - const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult; - expect(firedResults).toBe(60); - expect(noDataResults).toBe(0); - expect(errorResults).toBe(0); + const { fired, noData, error, notifications } = ungroupedResult; + expect(fired).toBe(60); + expect(noData).toBe(0); + expect(error).toBe(0); expect(notifications).toBe(60); }); test('returns the expected results using a throttle interval longer than the alert interval', async () => { @@ -63,10 +63,10 @@ describe('Previewing the metric threshold alert type', () => { alertThrottle: '3m', alertOnNoData: true, }); - const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult; - expect(firedResults).toBe(30); - expect(noDataResults).toBe(0); - expect(errorResults).toBe(0); + const { fired, noData, error, notifications } = ungroupedResult; + expect(fired).toBe(30); + expect(noData).toBe(0); + expect(error).toBe(0); expect(notifications).toBe(15); }); }); @@ -83,15 +83,25 @@ describe('Previewing the metric threshold alert type', () => { alertThrottle: '1m', alertOnNoData: true, }); - const [firedResultsA, noDataResultsA, errorResultsA, notificationsA] = resultA; - expect(firedResultsA).toBe(30); - expect(noDataResultsA).toBe(0); - expect(errorResultsA).toBe(0); + const { + fired: firedA, + noData: noDataA, + error: errorA, + notifications: notificationsA, + } = resultA; + expect(firedA).toBe(30); + expect(noDataA).toBe(0); + expect(errorA).toBe(0); expect(notificationsA).toBe(30); - const [firedResultsB, noDataResultsB, errorResultsB, notificationsB] = resultB; - expect(firedResultsB).toBe(60); - expect(noDataResultsB).toBe(0); - expect(errorResultsB).toBe(0); + const { + fired: firedB, + noData: noDataB, + error: errorB, + notifications: notificationsB, + } = resultB; + expect(firedB).toBe(60); + expect(noDataB).toBe(0); + expect(errorB).toBe(0); expect(notificationsB).toBe(60); }); }); @@ -113,10 +123,10 @@ describe('Previewing the metric threshold alert type', () => { alertThrottle: '1m', alertOnNoData: true, }); - const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult; - expect(firedResults).toBe(25); - expect(noDataResults).toBe(10); - expect(errorResults).toBe(0); + const { fired, noData, error, notifications } = ungroupedResult; + expect(fired).toBe(25); + expect(noData).toBe(10); + expect(error).toBe(0); expect(notifications).toBe(35); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index ac6372a94b1fe..b9fa6659d5fcd 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -14,6 +14,7 @@ import { import { ILegacyScopedClusterClient } from '../../../../../../../src/core/server'; import { InfraSource } from '../../../../common/http_api/source_api'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { PreviewResult } from '../common/types'; import { MetricExpressionParams } from './types'; import { evaluateAlert } from './lib/evaluate_alert'; @@ -39,7 +40,7 @@ export const previewMetricThresholdAlert: ( params: PreviewMetricThresholdAlertParams, iterations?: number, precalculatedNumberOfGroups?: number -) => Promise = async ( +) => Promise = async ( { callCluster, params, @@ -98,6 +99,7 @@ export const previewMetricThresholdAlert: ( numberOfResultBuckets / alertResultsPerExecution ); let numberOfTimesFired = 0; + let numberOfTimesWarned = 0; let numberOfNoDataResults = 0; let numberOfErrors = 0; let numberOfNotifications = 0; @@ -111,6 +113,9 @@ export const previewMetricThresholdAlert: ( const allConditionsFiredInMappedBucket = alertResults.every( (alertResult) => alertResult[group].shouldFire[mappedBucketIndex] ); + const allConditionsWarnInMappedBucket = + !allConditionsFiredInMappedBucket && + alertResults.every((alertResult) => alertResult[group].shouldWarn[mappedBucketIndex]); const someConditionsNoDataInMappedBucket = alertResults.some((alertResult) => { const hasNoData = alertResult[group].isNoData as boolean[]; return hasNoData[mappedBucketIndex]; @@ -131,6 +136,9 @@ export const previewMetricThresholdAlert: ( } else if (allConditionsFiredInMappedBucket) { numberOfTimesFired++; notifyWithThrottle(); + } else if (allConditionsWarnInMappedBucket) { + numberOfTimesWarned++; + notifyWithThrottle(); } else if (throttleTracker > 0) { throttleTracker += alertIntervalInSeconds; } @@ -138,7 +146,13 @@ export const previewMetricThresholdAlert: ( throttleTracker = 0; } } - return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors, numberOfNotifications]; + return { + fired: numberOfTimesFired, + warning: numberOfTimesWarned, + noData: numberOfNoDataResults, + error: numberOfErrors, + notifications: numberOfNotifications, + }; }) ); return previewResults; @@ -199,7 +213,12 @@ export const previewMetricThresholdAlert: ( .reduce((a, b) => { if (!a) return b; if (!b) return a; - return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]]; + const res = { ...a }; + const entries = (Object.entries(b) as unknown) as Array<[keyof PreviewResult, number]>; + for (const [key, value] of entries) { + res[key] += value; + } + return res; }) ); return zippedResult; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 6d8790f4f430c..e5e3a7bff329e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -15,7 +15,11 @@ import { ActionGroupIdsOf, } from '../../../../../alerts/server'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; -import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; +import { + createMetricThresholdExecutor, + FIRED_ACTIONS, + WARNING_ACTIONS, +} from './metric_threshold_executor'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; import { InfraBackendLibs } from '../../infra_types'; import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils'; @@ -37,7 +41,7 @@ export type MetricThresholdAlertType = AlertType< Record, AlertInstanceState, AlertInstanceContext, - ActionGroupIdsOf + ActionGroupIdsOf >; export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions< /** @@ -47,7 +51,7 @@ export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions< Record, AlertInstanceState, AlertInstanceContext, - ActionGroupIdsOf + ActionGroupIdsOf >; export function registerMetricThresholdAlertType(libs: InfraBackendLibs): MetricThresholdAlertType { @@ -56,6 +60,8 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs): Metric comparator: oneOfLiterals(Object.values(Comparator)), timeUnit: schema.string(), timeSize: schema.number(), + warningThreshold: schema.maybe(schema.arrayOf(schema.number())), + warningComparator: schema.maybe(oneOfLiterals(Object.values(Comparator))), }; const nonCountCriterion = schema.object({ @@ -92,7 +98,7 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs): Metric ), }, defaultActionGroupId: FIRED_ACTIONS.id, - actionGroups: [FIRED_ACTIONS], + actionGroups: [FIRED_ACTIONS, WARNING_ACTIONS], minimumLicenseRequired: 'basic', executor: createMetricThresholdExecutor(libs), actionVariables: { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index f876e40d9cd1f..37f21022f183d 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -29,6 +29,8 @@ interface BaseMetricExpressionParams { sourceId?: string; threshold: number[]; comparator: Comparator; + warningComparator?: Comparator; + warningThreshold?: number[]; } interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index ba16221108958..cc2cf4092520a 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { PreviewResult } from '../../lib/alerting/common/types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, @@ -65,29 +66,9 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) alertOnNoData, }); - const numberOfGroups = previewResult.length; - const resultTotals = previewResult.reduce( - (totals, [firedResult, noDataResult, errorResult, notifications]) => { - return { - ...totals, - fired: totals.fired + firedResult, - noData: totals.noData + noDataResult, - error: totals.error + errorResult, - notifications: totals.notifications + notifications, - }; - }, - { - fired: 0, - noData: 0, - error: 0, - notifications: 0, - } - ); + const payload = processPreviewResults(previewResult); return response.ok({ - body: alertPreviewSuccessResponsePayloadRT.encode({ - numberOfGroups, - resultTotals, - }), + body: alertPreviewSuccessResponsePayloadRT.encode(payload), }); } case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: { @@ -102,30 +83,10 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) alertOnNoData, }); - const numberOfGroups = previewResult.length; - const resultTotals = previewResult.reduce( - (totals, [firedResult, noDataResult, errorResult, notifications]) => { - return { - ...totals, - fired: totals.fired + firedResult, - noData: totals.noData + noDataResult, - error: totals.error + errorResult, - notifications: totals.notifications + notifications, - }; - }, - { - fired: 0, - noData: 0, - error: 0, - notifications: 0, - } - ); + const payload = processPreviewResults(previewResult); return response.ok({ - body: alertPreviewSuccessResponsePayloadRT.encode({ - numberOfGroups, - resultTotals, - }), + body: alertPreviewSuccessResponsePayloadRT.encode(payload), }); } default: @@ -150,3 +111,27 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }) ); }; + +const processPreviewResults = (previewResult: PreviewResult[]) => { + const numberOfGroups = previewResult.length; + const resultTotals = previewResult.reduce( + (totals, { fired, warning, noData, error, notifications }) => { + return { + ...totals, + fired: totals.fired + fired, + warning: totals.warning + warning, + noData: totals.noData + noData, + error: totals.error + error, + notifications: totals.notifications + notifications, + }; + }, + { + fired: 0, + warning: 0, + noData: 0, + error: 0, + notifications: 0, + } + ); + return { numberOfGroups, resultTotals }; +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 278294dea9449..6e9d0329eaff8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10175,10 +10175,6 @@ "xpack.infra.metrics.alertFlyout.alertPreviewError": "このアラート条件をプレビューするときにエラーが発生しました", "xpack.infra.metrics.alertFlyout.alertPreviewErrorDesc": "しばらくたってから再試行するか、詳細を確認してください。", "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "一部のデータを評価するときにエラーが発生しました。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "すべてを対象にする", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データなしの件数:{boldedResultsNumber}", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData, plural, other {件の結果がありました}}", - "xpack.infra.metrics.alertFlyout.alertPreviewResult": "{firedTimes} 回発生しました", "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotifications": "結果として、このアラートは、「{alertThrottle}」に関して選択した[通知間隔]設定に基づいて{notifications}を送信しました。", "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber": "{notifs, plural, other {#通知}}", "xpack.infra.metrics.alertFlyout.conditions": "条件", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4704cb07d27b0..eeda709104479 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10202,12 +10202,7 @@ "xpack.infra.metrics.alertFlyout.alertPreviewError": "尝试预览此告警条件时发生错误", "xpack.infra.metrics.alertFlyout.alertPreviewErrorDesc": "请稍后重试或查看详情了解更多信息。", "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "尝试评估部分数据时发生错误。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups, plural,other {# 个 {groupName}}}", - "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "在", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在 {boldedResultsNumber}无数据结果。", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData, plural, other {# 个结果}}", - "xpack.infra.metrics.alertFlyout.alertPreviewResult": "有 {firedTimes}", - "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "在过去 {lookback} 满足此告警的条件。", + "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "在 {numberOfGroups, plural,other {# 个 {groupName}}}", "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotifications": "因此,此告警将根据“{alertThrottle}”的选定“通知频率”设置发送{notifications}。", "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber": "{notifs, plural, other {# 个通知}}", "xpack.infra.metrics.alertFlyout.conditions": "条件", From ad2f5d7918cf0e5cadfeba7fb7671abe469f58e8 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 9 Feb 2021 15:19:08 -0600 Subject: [PATCH 076/100] [Workplace Search] Fix paths to nest correctly (#90831) We have serveral places that I went up one level too many when getting shared components. This refactors that to align with other usages. --- .../components/add_source/add_source_list.test.tsx | 2 +- .../content_sources/components/add_source/add_source_list.tsx | 2 +- .../components/add_source/configure_oauth.test.tsx | 2 +- .../content_sources/components/add_source/configure_oauth.tsx | 4 ++-- .../components/add_source/connect_instance.tsx | 2 +- .../content_sources/components/add_source/re_authenticate.tsx | 2 +- .../content_sources/components/add_source/save_config.tsx | 2 +- .../content_sources/components/add_source/source_features.tsx | 2 +- .../views/content_sources/components/source_content.test.tsx | 2 +- .../views/content_sources/components/source_content.tsx | 2 +- .../views/content_sources/private_sources.tsx | 2 +- .../workplace_search/views/content_sources/sources_router.tsx | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx index 6da348c6e2755..90da349ea4f27 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx @@ -19,7 +19,7 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; -import { Loading } from '../../../../../../applications/shared/loading'; +import { Loading } from '../../../../../shared/loading'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { AddSourceList } from './add_source_list'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index d026782d12540..372187485f277 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -19,7 +19,7 @@ import { EuiEmptyPrompt, } from '@elastic/eui'; -import { Loading } from '../../../../../../applications/shared/loading'; +import { Loading } from '../../../../../shared/loading'; import { AppLogic } from '../../../../app_logic'; import noSharedSourcesIcon from '../../../../assets/share_circle.svg'; import { ContentSection } from '../../../../components/shared/content_section'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx index 985488558c984..533dfcda70db1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx @@ -14,7 +14,7 @@ import { shallow } from 'enzyme'; import { EuiCheckboxGroup } from '@elastic/eui'; -import { Loading } from '../../../../../../applications/shared/loading'; +import { Loading } from '../../../../../shared/loading'; import { ConfigureOauth } from './configure_oauth'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx index eb7b61ef658db..69a2fbd1495c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx @@ -21,8 +21,8 @@ import { } from '@elastic/eui'; import { EuiCheckboxGroupIdToSelectedMap } from '@elastic/eui/src/components/form/checkbox/checkbox_group'; -import { Loading } from '../../../../../../applications/shared/loading'; -import { parseQueryParams } from '../../../../../../applications/shared/query_params'; +import { Loading } from '../../../../../shared/loading'; +import { parseQueryParams } from '../../../../../shared/query_params'; import { AddSourceLogic } from './add_source_logic'; import { CONFIG_OAUTH_LABEL, CONFIG_OAUTH_BUTTON } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index d85bd21c54e5a..08b29075f3d0d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -28,7 +28,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { LicensingLogic } from '../../../../../../applications/shared/licensing'; +import { LicensingLogic } from '../../../../../shared/licensing'; import { AppLogic } from '../../../../app_logic'; import { DOCUMENT_PERMISSIONS_DOCS_URL } from '../../../../routes'; import { FeatureIds, Configuration, Features } from '../../../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx index 15082f6de85bf..eb6736d84a197 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx @@ -14,7 +14,7 @@ import { useActions, useValues } from 'kea'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { parseQueryParams } from '../../../../../../applications/shared/query_params'; +import { parseQueryParams } from '../../../../../shared/query_params'; import { AddSourceLogic } from './add_source_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx index 06ee2b6eb40f1..956d5143ef2c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx @@ -22,7 +22,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { LicensingLogic } from '../../../../../../applications/shared/licensing'; +import { LicensingLogic } from '../../../../../shared/licensing'; import { ApiKey } from '../../../../components/shared/api_key'; import { PUBLIC_KEY_LABEL, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx index f304a1a36d9dd..0838ab2ccdae2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { LicensingLogic } from '../../../../../../applications/shared/licensing'; +import { LicensingLogic } from '../../../../../shared/licensing'; import { AppLogic } from '../../../../app_logic'; import { LicenseBadge } from '../../../../components/shared/license_badge'; import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../../routes'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx index e904efb73afc8..12399d4822a13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx @@ -24,8 +24,8 @@ import { EuiLink, } from '@elastic/eui'; -import { Loading } from '../../../../../applications/shared/loading'; import { DEFAULT_META } from '../../../../shared/constants'; +import { Loading } from '../../../../shared/loading'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx index bf18f88e47537..3dd8ad1dc7899 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -31,7 +31,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Loading } from '../../../../../applications/shared/loading'; +import { Loading } from '../../../../shared/loading'; import { TruncatedContent } from '../../../../shared/truncate'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx index 61be7b6281e9e..087681fa89603 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx @@ -12,7 +12,7 @@ import { useActions, useValues } from 'kea'; import { EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { LicensingLogic } from '../../../../applications/shared/licensing'; +import { LicensingLogic } from '../../../shared/licensing'; import { Loading } from '../../../shared/loading'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { AppLogic } from '../../app_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index dcc15be4462c7..b7857cf4612a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -11,9 +11,9 @@ import { Redirect, Route, Switch, useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions, useValues } from 'kea'; -import { LicensingLogic } from '../../../../applications/shared/licensing'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { LicensingLogic } from '../../../shared/licensing'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { AppLogic } from '../../app_logic'; import { NAV } from '../../constants'; From 10adb72ddbf56b7d3a060315b9559900bea2adfc Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Tue, 9 Feb 2021 16:27:36 -0500 Subject: [PATCH 077/100] Plugin introduction doc improvements (#90502) * More plugin docs improvements * remove speculation about the future * remove registries section Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- dev_docs/assets/applications.png | Bin 0 -> 199244 bytes dev_docs/assets/platform_plugins_core.png | Bin 0 -> 5340 bytes dev_docs/kibana_platform_plugin_intro.mdx | 291 +++++----------------- dev_docs/tutorials/building_a_plugin.mdx | 226 +++++++++++++++++ 4 files changed, 291 insertions(+), 226 deletions(-) create mode 100644 dev_docs/assets/applications.png create mode 100644 dev_docs/assets/platform_plugins_core.png create mode 100644 dev_docs/tutorials/building_a_plugin.mdx diff --git a/dev_docs/assets/applications.png b/dev_docs/assets/applications.png new file mode 100644 index 0000000000000000000000000000000000000000..409f3416136ecad21224af9ffd9effc71d355b0d GIT binary patch literal 199244 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

    + +

    + + + + + + + + + + + + + + + + + + + + + + ); +}; + +// required for dynamic import +// eslint-disable-next-line import/no-default-export +export default Expression; + +const StyledExpressionRow = euiStyled(EuiFlexGroup)` + display: flex; + flex-wrap: wrap; + margin: 0 -4px; +`; + +const StyledExpression = euiStyled.div` + padding: 0 4px; +`; + +const getDisplayNameForType = (type: InventoryItemType) => { + const inventoryModel = findInventoryModel(type); + return inventoryModel.displayName; +}; + +export const nodeTypes: { [key: string]: any } = { + hosts: { + text: getDisplayNameForType('host'), + value: 'hosts', + }, + k8s: { + text: getDisplayNameForType('pod'), + value: 'k8s', + }, +}; + +const getMLMetricFromInventoryMetric = (metric: SnapshotMetricType) => { + switch (metric) { + case 'memory': + return 'memory_usage'; + case 'tx': + return 'network_out'; + case 'rx': + return 'network_in'; + default: + return null; + } +}; + +const getMLNodeTypeFromInventoryNodeType = (nodeType: InventoryItemType) => { + switch (nodeType) { + case 'host': + return 'hosts'; + case 'pod': + return 'k8s'; + default: + return null; + } +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx new file mode 100644 index 0000000000000..34a917a77dcf5 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { debounce } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import { first } from 'lodash'; +import { EuiFlexGroup, EuiFormRow, EuiCheckbox, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { + MetricsExplorerKueryBar, + CurryLoadSuggestionsType, +} from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; + +interface Props { + fieldName: string; + fieldValue: string; + nodeType: MetricAnomalyParams['nodeType']; + onChangeFieldName: (v: string) => void; + onChangeFieldValue: (v: string) => void; + derivedIndexPattern: Parameters[0]['derivedIndexPattern']; +} + +const FILTER_TYPING_DEBOUNCE_MS = 500; + +export const InfluencerFilter = ({ + fieldName, + fieldValue, + nodeType, + onChangeFieldName, + onChangeFieldValue, + derivedIndexPattern, +}: Props) => { + const fieldNameOptions = useMemo(() => (nodeType === 'k8s' ? k8sFieldNames : hostFieldNames), [ + nodeType, + ]); + + // If initial props contain a fieldValue, assume it was passed in from loaded alertParams, + // and enable the UI element + const [isEnabled, updateIsEnabled] = useState(fieldValue ? true : false); + const [storedFieldValue, updateStoredFieldValue] = useState(fieldValue); + + useEffect( + () => + nodeType === 'k8s' + ? onChangeFieldName(first(k8sFieldNames)!.value) + : onChangeFieldName(first(hostFieldNames)!.value), + [nodeType, onChangeFieldName] + ); + + const onSelectFieldName = useCallback((e) => onChangeFieldName(e.target.value), [ + onChangeFieldName, + ]); + const onUpdateFieldValue = useCallback( + (value) => { + updateStoredFieldValue(value); + onChangeFieldValue(value); + }, + [onChangeFieldValue] + ); + + const toggleEnabled = useCallback(() => { + const nextState = !isEnabled; + updateIsEnabled(nextState); + if (!nextState) { + onChangeFieldValue(''); + } else { + onChangeFieldValue(storedFieldValue); + } + }, [isEnabled, updateIsEnabled, onChangeFieldValue, storedFieldValue]); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnUpdateFieldValue = useCallback( + debounce(onUpdateFieldValue, FILTER_TYPING_DEBOUNCE_MS), + [onUpdateFieldValue] + ); + + const affixFieldNameToQuery: CurryLoadSuggestionsType = (fn) => ( + expression, + cursorPosition, + maxSuggestions + ) => { + // Add the field name to the front of the passed-in query + const prefix = `${fieldName}:`; + // Trim whitespace to prevent AND/OR suggestions + const modifiedExpression = `${prefix}${expression}`.trim(); + // Move the cursor position forward by the length of the field name + const modifiedPosition = cursorPosition + prefix.length; + return fn(modifiedExpression, modifiedPosition, maxSuggestions, (suggestions) => + suggestions + .map((s) => ({ + ...s, + // Remove quotes from suggestions + text: s.text.replace(/\"/g, '').trim(), + // Offset the returned suggestions' cursor positions so that they can be autocompleted accurately + start: s.start - prefix.length, + end: s.end - prefix.length, + })) + // Removing quotes can lead to an already-selected suggestion still coming up in the autocomplete list, + // so filter these out + .filter((s) => !expression.startsWith(s.text)) + ); + }; + + return ( + + } + helpText={ + isEnabled ? ( + <> + {i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpText', { + defaultMessage: + 'Limit the scope of your alert trigger to anomalies influenced by certain node(s).', + })} +
    + {i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpTextExample', { + defaultMessage: 'For example: "my-node-1" or "my-node-*"', + })} + + ) : null + } + fullWidth + display="rowCompressed" + > + {isEnabled ? ( + + + + + + + + + ) : ( + <> + )} +
    + ); +}; + +const hostFieldNames = [ + { + value: 'host.name', + text: 'host.name', + }, +]; + +const k8sFieldNames = [ + { + value: 'kubernetes.pod.uid', + text: 'kubernetes.pod.uid', + }, + { + value: 'kubernetes.node.name', + text: 'kubernetes.node.name', + }, + { + value: 'kubernetes.namespace', + text: 'kubernetes.namespace', + }, +]; + +const filterByNodeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.filterByNodeLabel', { + defaultMessage: 'Filter by node', +}); diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx new file mode 100644 index 0000000000000..6ddcf8fd5cb65 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; + +type Node = MetricAnomalyParams['nodeType']; + +interface WhenExpressionProps { + value: Node; + options: { [key: string]: { text: string; value: Node } }; + onChange: (value: Node) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const NodeTypeExpression = ({ + value, + options, + onChange, + popupPosition, +}: WhenExpressionProps) => { + const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); + + return ( + { + setAggTypePopoverOpen(true); + }} + /> + } + isOpen={aggTypePopoverOpen} + closePopover={() => { + setAggTypePopoverOpen(false); + }} + ownFocus + anchorPosition={popupPosition ?? 'downLeft'} + > +
    + setAggTypePopoverOpen(false)}> + + + { + onChange(e.target.value as Node); + setAggTypePopoverOpen(false); + }} + options={Object.values(options).map((o) => o)} + /> +
    +
    + ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + + + {children} + + onClose()} + /> + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx new file mode 100644 index 0000000000000..2dc561ff172b9 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; + +interface WhenExpressionProps { + value: Exclude; + onChange: (value: ANOMALY_THRESHOLD) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +const options = { + [ANOMALY_THRESHOLD.CRITICAL]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.criticalLabel', { + defaultMessage: 'Critical', + }), + value: ANOMALY_THRESHOLD.CRITICAL, + }, + [ANOMALY_THRESHOLD.MAJOR]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.majorLabel', { + defaultMessage: 'Major', + }), + value: ANOMALY_THRESHOLD.MAJOR, + }, + [ANOMALY_THRESHOLD.MINOR]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.minorLabel', { + defaultMessage: 'Minor', + }), + value: ANOMALY_THRESHOLD.MINOR, + }, + [ANOMALY_THRESHOLD.WARNING]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.warningLabel', { + defaultMessage: 'Warning', + }), + value: ANOMALY_THRESHOLD.WARNING, + }, +}; + +export const SeverityThresholdExpression = ({ + value, + onChange, + popupPosition, +}: WhenExpressionProps) => { + const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); + + return ( + { + setAggTypePopoverOpen(true); + }} + /> + } + isOpen={aggTypePopoverOpen} + closePopover={() => { + setAggTypePopoverOpen(false); + }} + ownFocus + anchorPosition={popupPosition ?? 'downLeft'} + > +
    + setAggTypePopoverOpen(false)}> + + + { + onChange(Number(e.target.value) as ANOMALY_THRESHOLD); + setAggTypePopoverOpen(false); + }} + options={Object.values(options).map((o) => o)} + /> +
    +
    + ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + + + {children} + + onClose()} + /> + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx new file mode 100644 index 0000000000000..8e254fb2b67a8 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; + +export function validateMetricAnomaly({ + hasInfraMLCapabilities, +}: { + hasInfraMLCapabilities: boolean; +}): ValidationResult { + const validationResult = { errors: {} }; + const errors: { + hasInfraMLCapabilities: string[]; + } = { + hasInfraMLCapabilities: [], + }; + + validationResult.errors = errors; + + if (!hasInfraMLCapabilities) { + errors.hasInfraMLCapabilities.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.mlCapabilitiesRequired', { + defaultMessage: 'Cannot create an anomaly alert when machine learning is disabled.', + }) + ); + } + + return validationResult; +} diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts new file mode 100644 index 0000000000000..31fed514bdacc --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../common/alerting/metrics'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { AlertTypeParams } from '../../../../alerts/common'; +import { validateMetricAnomaly } from './components/validation'; + +interface MetricAnomalyAlertTypeParams extends AlertTypeParams { + hasInfraMLCapabilities: boolean; +} + +export function createMetricAnomalyAlertType(): AlertTypeModel { + return { + id: METRIC_ANOMALY_ALERT_TYPE_ID, + description: i18n.translate('xpack.infra.metrics.anomaly.alertFlyout.alertDescription', { + defaultMessage: 'Alert when the anomaly score exceeds a defined threshold.', + }), + iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/metric-anomaly-alert.html`; + }, + alertParamsExpression: React.lazy(() => import('./components/expression')), + validate: validateMetricAnomaly, + defaultActionMessage: i18n.translate( + 'xpack.infra.metrics.alerting.anomaly.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} is in a state of \\{\\{context.alertState\\}\\} + +\\{\\{context.metric\\}\\} was \\{\\{context.summary\\}\\} than normal at \\{\\{context.timestamp\\}\\} + +Typical value: \\{\\{context.typical\\}\\} +Actual value: \\{\\{context.actual\\}\\} +`, + } + ), + requiresAppContext: false, + }; +} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx deleted file mode 100644 index 3bbe811225825..0000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ /dev/null @@ -1,57 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useCallback } from 'react'; -import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useAlertPrefillContext } from '../../use_alert_prefill'; -import { AlertFlyout } from './alert_flyout'; -import { ManageAlertsContextMenuItem } from '../../inventory/components/manage_alerts_context_menu_item'; - -export const MetricsAlertDropdown = () => { - const [popoverOpen, setPopoverOpen] = useState(false); - const [flyoutVisible, setFlyoutVisible] = useState(false); - - const { metricThresholdPrefill } = useAlertPrefillContext(); - const { groupBy, filterQuery, metrics } = metricThresholdPrefill; - - const closePopover = useCallback(() => { - setPopoverOpen(false); - }, [setPopoverOpen]); - - const openPopover = useCallback(() => { - setPopoverOpen(true); - }, [setPopoverOpen]); - - const menuItems = [ - setFlyoutVisible(true)}> - - , - , - ]; - - return ( - <> - - - - } - isOpen={popoverOpen} - closePopover={closePopover} - > - - - - - ); -}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index 929654ecb4693..e7e4ade5257fc 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -7,10 +7,10 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; interface Props { visible?: boolean; @@ -42,3 +42,10 @@ export const AlertFlyout = (props: Props) => { return <>{visible && AddAlertFlyout}; }; + +export const PrefilledThresholdAlertFlyout = ({ onClose }: { onClose(): void }) => { + const { metricThresholdPrefill } = useAlertPrefillContext(); + const { groupBy, filterQuery, metrics } = metricThresholdPrefill; + + return ; +}; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts index 1bcc9e7157a51..db5a996c604fc 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts @@ -14,4 +14,3 @@ export * from './missing_results_privileges_prompt'; export * from './missing_setup_privileges_prompt'; export * from './ml_unavailable_prompt'; export * from './setup_status_unknown_prompt'; -export * from './subscription_splash_content'; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx deleted file mode 100644 index c91c1d82afe9b..0000000000000 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx +++ /dev/null @@ -1,176 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, - EuiText, - EuiButton, - EuiButtonEmpty, - EuiImage, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { HttpStart } from 'src/core/public'; -import { LoadingPage } from '../../loading_page'; - -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { useTrialStatus } from '../../../hooks/use_trial_status'; - -export const SubscriptionSplashContent: React.FC = () => { - const { services } = useKibana<{ http: HttpStart }>(); - const { loadState, isTrialAvailable, checkTrialAvailability } = useTrialStatus(); - - useEffect(() => { - checkTrialAvailability(); - }, [checkTrialAvailability]); - - if (loadState === 'pending') { - return ( - - ); - } - - const canStartTrial = isTrialAvailable && loadState === 'resolved'; - - let title; - let description; - let cta; - - if (canStartTrial) { - title = ( - - ); - - description = ( - - ); - - cta = ( - - - - ); - } else { - title = ( - - ); - - description = ( - - ); - - cta = ( - - - - ); - } - - return ( - - - - - - -

    {title}

    -
    - - -

    {description}

    -
    - -
    {cta}
    -
    - - - -
    - - -

    - -

    -
    - - - -
    -
    -
    -
    - ); -}; - -const SubscriptionPage = euiStyled(EuiPage)` - height: 100% -`; - -const SubscriptionPageContent = euiStyled(EuiPageContent)` - max-width: 768px !important; -`; - -const SubscriptionPageFooter = euiStyled.div` - background: ${(props) => props.theme.eui.euiColorLightestShade}; - margin: 0 -${(props) => props.theme.eui.paddingSizes.l} -${(props) => - props.theme.eui.paddingSizes.l}; - padding: ${(props) => props.theme.eui.paddingSizes.l}; -`; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index 4b609a881bd18..e63f43470497d 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -75,7 +75,7 @@ export const SourceConfigurationSettings = ({ source, ]); - const { hasInfraMLCapabilites } = useInfraMLCapabilitiesContext(); + const { hasInfraMLCapabilities } = useInfraMLCapabilitiesContext(); if ((isLoading || isUninitialized) && !source) { return ; @@ -128,7 +128,7 @@ export const SourceConfigurationSettings = ({ /> - {hasInfraMLCapabilites && ( + {hasInfraMLCapabilities && ( <> { const { services } = useKibana<{ http: HttpStart }>(); @@ -102,58 +102,60 @@ export const SubscriptionSplashContent: React.FC = () => { } return ( - - - - - - -

    {title}

    + + + + + + + +

    {title}

    +
    + + +

    {description}

    +
    + +
    {cta}
    +
    + + + +
    + + +

    + +

    - - -

    {description}

    -
    - -
    {cta}
    -
    - - - -
    - - -

    + -

    -
    - - - -
    -
    -
    -
    + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx index 72dc4da01d867..661ce8f8a253c 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx @@ -52,11 +52,11 @@ export const useInfraMLCapabilities = () => { const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob; const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs; - const hasInfraMLCapabilites = + const hasInfraMLCapabilities = mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace; return { - hasInfraMLCapabilites, + hasInfraMLCapabilities, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities, isLoading, diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx index 379ac9774c242..1a759950f640d 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -56,7 +56,8 @@ class WithKueryAutocompletionComponent extends React.Component< private loadSuggestions = async ( expression: string, cursorPosition: number, - maxSuggestions?: number + maxSuggestions?: number, + transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[] ) => { const { indexPattern } = this.props; const language = 'kuery'; @@ -86,6 +87,10 @@ class WithKueryAutocompletionComponent extends React.Component< boolFilter: [], })) || []; + const transformedSuggestions = transformSuggestions + ? transformSuggestions(suggestions) + : suggestions; + this.setState((state) => state.currentRequest && state.currentRequest.expression !== expression && @@ -94,7 +99,9 @@ class WithKueryAutocompletionComponent extends React.Component< : { ...state, currentRequest: null, - suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions, + suggestions: maxSuggestions + ? transformedSuggestions.slice(0, maxSuggestions) + : transformedSuggestions, } ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index f0fdd79bcd93d..628df397998ee 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback, useEffect } from 'react'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, - SubscriptionSplashContent, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index 4d06d23ef93ef..5fd00527b8b70 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import React, { memo, useEffect, useCallback } from 'react'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, - SubscriptionSplashContent, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 52c2a70f2d359..8fd32bda7fbc8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -35,12 +35,11 @@ import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; -import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown'; -import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; +import { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown'; import { SavedView } from '../../containers/saved_view/saved_view'; import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities'; -import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout'; +import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout'; import { HeaderMenuPortal } from '../../../../observability/public'; import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; @@ -83,8 +82,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {

    k2nK`W(u5cSo@}8xbyhCYgFZ%PgeSp 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 provides basic utilities to search, query, and filter data in Elasticsearch. -This code is not part of Core, but is still fundamental for building a plugin, - and we strongly encourage using this service over querying Elasticsearch directly. - - -We currently have three kinds of public services: - - - platform services provided by `core` - - platform services provided by plugins, that can, and should, be used by every plugin (e.g. ) . - - shared services provided by plugins, that are only relevant for only a few, specific plugins (e.g. "presentation utils"). - -Two common questions we encounter are: + - Platform services provided by `core` () + - Platform services provided by plugins () + - Shared services provided by plugins, that are only relevant for only a few, specific plugins (e.g. "presentation utils"). -1. Which services are platform services? -2. What is the difference between platform code supplied by core, and platform code supplied by plugins? + The first two items are what make up "Platform services". -We don't have great answers to those questions today. Currently, the best answers we have are: + -1. Platform plugins are _usually_ plugins that are managed by the Platform Group, but we are starting to see some exceptions. -2. `core` code contains the most fundamental and stable services needed for plugin development. Everything else goes in a plugin. +We try to put only the most stable and fundamental code into `Core`, while more application focused functionality goes in a plugin, but the heuristic isn't +clear, and we haven't done a great job of sticking to it. For example, notifications and toasts are core services, but data and search are plugin services. -We will continue to focus on adding clarity around these types of services and what developers can expect from each. - - +Today it looks something like this. +![Core vs platform plugins vs plugins](assets/platform_plugins_core.png) + - When the Kibana platform and plugin infrastructure was built, we thought of two types of code: core services, and other plugin services. We planned to keep the most stable and fundamental code needed to build plugins inside core. @@ -70,125 +59,62 @@ Another side effect of having many small plugins is that common code often ends We recognize the need to better clarify the relationship between core functionality, platform-like plugin functionality, and functionality exposed by other plugins. It's something we will be working on! - -The main difference between core functionality and functionality supplied by plugins, is in how it is accessed. Core is -passed to plugins as the first parameter to their `start` and `setup` lifecycle functions, while plugin supplied functionality is passed as the -second parameter. Plugin dependencies must be declared explicitly inside the `kibana.json` file. Core functionality is always provided. Read the -section on for more information. - -## The anatomy of a plugin - -Plugins are defined as classes and present themselves to Kibana through a simple wrapper function. A plugin can have browser-side code, server-side code, -or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly, -and you interact with Core and other plugins in the same way. - -The basic file structure of a Kibana plugin named demo that has both client-side and server-side code would be: - -``` -plugins/ - demo - kibana.json [1] - public - index.ts [2] - plugin.ts [3] - server - index.ts [4] - plugin.ts [5] -``` - -### [1] kibana.json - -`kibana.json` is a static manifest file that is used to identify the plugin and to specify if this plugin has server-side code, browser-side code, or both: - -``` -{ - "id": "demo", - "version": "kibana", - "server": true, - "ui": true -} -``` - -### [2] public/index.ts - -`public/index.ts` is the entry point into the client-side code of this plugin. It must export a function named plugin, which will receive a standard set of - core capabilities as an argument. It should return an instance of its plugin class for Kibana to load. - -``` -import type { PluginInitializerContext } from 'kibana/server'; -import { DemoPlugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new DemoPlugin(initializerContext); -} -``` - -### [3] public/plugin.ts - -`public/plugin.ts` is the client-side plugin definition itself. Technically speaking, it does not need to be a class or even a separate file from the entry - point, but all plugins at Elastic should be consistent in this way. - +We will continue to focus on adding clarity around these types of services and what developers can expect from each. - ```ts -import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; -export class DemoPlugin implements Plugin { - constructor(initializerContext: PluginInitializerContext) {} + - public setup(core: CoreSetup) { - // called when plugin is setting up during Kibana's startup sequence - } +### Core services - public start(core: CoreStart) { - // called after all plugins are set up - } +Sometimes referred to just as Core, Core services provide the most basic and fundamental tools neccessary for building a plugin, like creating saved objects, +routing, application registration, and notifications. The Core platform is not a plugin itself, although +there are some plugins that provide platform functionality. We call these . - public stop() { - // called when plugin is torn down during Kibana's shutdown sequence - } -} - ``` +### Platform plugins +Plugins that provide fundamental services and functionality to extend and customize Kibana, for example, the + plugin. There is no official way to tell if a plugin is a platform plugin or not. +Platform plugins are _usually_ plugins that are managed by the Platform Group, but we are starting to see some exceptions. -### [4] server/index.ts +## Plugins -`server/index.ts` is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point: +Plugins are code that is written to extend and customize Kibana. Plugin's don't have to be part of the Kibana repo, though the Kibana +repo does contain many plugins! Plugins add customizations by +using provided by . +Sometimes people confuse the term "plugin" and "application". While often there is a 1:1 relationship between a plugin and an application, it is not always the case. +A plugin may register many applications, or none. -### [5] server/plugin.ts +### Applications -`server/plugin.ts` is the server-side plugin definition. The shape of this plugin is the same as it’s client-side counter-part: +Applications are top level pages in the Kibana UI. Dashboard, Canvas, Maps, App Search, etc, are all examples of applications: -```ts -import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; +![applications in kibana](./assets/applications.png) -export class DemoPlugin implements Plugin { - constructor(initializerContext: PluginInitializerContext) {} +A plugin can register an application by +adding it to core's application . - public setup(core: CoreSetup) { - // called when plugin is setting up during Kibana's startup sequence - } +### Public plugin API - public start(core: CoreStart) { - // called after all plugins are set up - } +A plugin's public API consists of everything exported from a plugin's , +as well as from the top level `index.ts` files that exist in the three "scope" folders: - public stop() { - // called when plugin is torn down during Kibana's shutdown sequence - } -} -``` +- common/index.ts +- public/index.ts +- server/index.ts -Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain -considerations related to how plugins integrate with core APIs and APIs exposed by other plugins that may greatly impact how they are built. +Any plugin that exports something from those files, or from the lifecycle methods, is exposing a public service. We sometimes call these things "plugin services" or +"shared services". -## Plugin lifecycles & Core services +## Lifecycle methods -The various independent domains that make up core are represented by a series of services. Those services expose public interfaces that are provided to all plugins. -Services expose different features at different parts of their lifecycle. We describe the lifecycle of core services and plugins with specifically-named functions on the service definition. +Core, and plugins, expose different features at different parts of their lifecycle. We describe the lifecycle of core services and plugins with + specifically-named functions on the service definition. -Kibana has three lifecycles: setup, start, and stop. Each plugin’s setup function is called sequentially while Kibana is setting up on the server or when it is being loaded in the browser. The start functions are called sequentially after setup has been completed for all plugins. The stop functions are called sequentially while Kibana is gracefully shutting down the server or when the browser tab or window is being closed. +Kibana has three lifecycles: setup, start, and stop. Each plugin’s setup function is called sequentially while Kibana is setting up + on the server or when it is being loaded in the browser. The start functions are called sequentially after setup has been completed for all plugins. + The stop functions are called sequentially while Kibana is gracefully shutting down the server or when the browser tab or window is being closed. The table below explains how each lifecycle relates to the state of Kibana. @@ -201,105 +127,18 @@ The table below explains how each lifecycle relates to the state of Kibana. Different service interfaces can and will be passed to setup, start, and stop because certain functionality makes sense in the context of a running plugin while other types of functionality may have restrictions or may only make sense in the context of a plugin that is stopping. -## How plugin's interact with each other, and Core - -The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. -For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler, -a plugin just accesses it off of the first argument: - -```ts -import type { CoreSetup } from 'kibana/server'; - -export class DemoPlugin { - public setup(core: CoreSetup) { - const router = core.http.createRouter(); - // handler is called when '/path' resource is requested with `GET` method - router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); - } -} -``` - -Unlike core, capabilities exposed by plugins are not automatically injected into all plugins. -Instead, if a plugin wishes to use the public interface provided by another plugin, it must first declare that plugin as a - dependency in it’s kibana.json manifest file. - -** foobar plugin.ts: ** - -``` -import type { Plugin } from 'kibana/server'; -export interface FoobarPluginSetup { [1] - getFoo(): string; -} - -export interface FoobarPluginStart { [1] - getBar(): string; -} - -export class MyPlugin implements Plugin { - public setup(): FoobarPluginSetup { - return { - getFoo() { - return 'foo'; - }, - }; - } - - public start(): FoobarPluginStart { - return { - getBar() { - return 'bar'; - }, - }; - } -} -``` -[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins. - - -** demo kibana.json** - -``` -{ - "id": "demo", - "requiredPlugins": ["foobar"], - "server": true, - "ui": true -} -``` - -With that specified in the plugin manifest, the appropriate interfaces are then available via the second argument of setup and/or start: - -```ts -import type { CoreSetup, CoreStart } from 'kibana/server'; -import type { FoobarPluginSetup, FoobarPluginStart } from '../../foobar/server'; - -interface DemoSetupPlugins { [1] - foobar: FoobarPluginSetup; -} - -interface DemoStartPlugins { - foobar: FoobarPluginStart; -} - -export class DemoPlugin { - public setup(core: CoreSetup, plugins: DemoSetupPlugins) { [2] - const { foobar } = plugins; - foobar.getFoo(); // 'foo' - foobar.getBar(); // throws because getBar does not exist - } - - public start(core: CoreStart, plugins: DemoStartPlugins) { [3] - const { foobar } = plugins; - foobar.getFoo(); // throws because getFoo does not exist - foobar.getBar(); // 'bar' - } - - public stop() {} -} -``` - -[1] The interface for plugin’s dependencies must be manually composed. You can do this by importing the appropriate type from the plugin and constructing an interface where the property name is the plugin’s ID. - -[2] These manually constructed types should then be used to specify the type of the second argument to the plugin. - -[3] Notice that the type for the setup and start lifecycles are different. Plugin lifecycle functions can only access the APIs that are exposed during that lifecycle. +## Extension points + +An extension point is a function provided by core, or a plugin's plugin API, that can be used by other +plugins to customize the Kibana experience. Examples of extension points are: + +- core.application.register (The extension point talked about above) +- core.notifications.toasts.addSuccess +- core.overlays.showModal +- embeddables.registerEmbeddableFactory +- uiActions.registerAction +- core.saedObjects.registerType + +## Follow up material + +Learn how to build your own plugin by following \ No newline at end of file diff --git a/dev_docs/tutorials/building_a_plugin.mdx b/dev_docs/tutorials/building_a_plugin.mdx new file mode 100644 index 0000000000000..cee5a9a399de5 --- /dev/null +++ b/dev_docs/tutorials/building_a_plugin.mdx @@ -0,0 +1,226 @@ +--- +id: kibDevTutorialBuildAPlugin +slug: /kibana-dev-docs/tutorials/build-a-plugin +title: Kibana plugin tutorial +summary: Anatomy of a Kibana plugin and how to build one +date: 2021-02-05 +tags: ['kibana','onboarding', 'dev', 'tutorials'] +--- + +Prereading material: + +- + +## The anatomy of a plugin + +Plugins are defined as classes and present themselves to Kibana through a simple wrapper function. A plugin can have browser-side code, server-side code, +or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly, +and you interact with Core and other plugins in the same way. + +The basic file structure of a Kibana plugin named demo that has both client-side and server-side code would be: + +``` +plugins/ + demo + kibana.json [1] + public + index.ts [2] + plugin.ts [3] + server + index.ts [4] + plugin.ts [5] + common + index.ts [6] +``` + +### [1] kibana.json + +`kibana.json` is a static manifest file that is used to identify the plugin and to specify if this plugin has server-side code, browser-side code, or both: + +``` +{ + "id": "demo", + "version": "kibana", + "server": true, + "ui": true +} +``` + +### [2] public/index.ts + +`public/index.ts` is the entry point into the client-side code of this plugin. It must export a function named plugin, which will receive a standard set of + core capabilities as an argument. It should return an instance of its plugin class for Kibana to load. + +``` +import type { PluginInitializerContext } from 'kibana/server'; +import { DemoPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new DemoPlugin(initializerContext); +} +``` + +### [3] public/plugin.ts + +`public/plugin.ts` is the client-side plugin definition itself. Technically speaking, it does not need to be a class or even a separate file from the entry + point, but all plugins at Elastic should be consistent in this way. + + + ```ts +import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; + +export class DemoPlugin implements Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup) { + // called when plugin is setting up during Kibana's startup sequence + } + + public start(core: CoreStart) { + // called after all plugins are set up + } + + public stop() { + // called when plugin is torn down during Kibana's shutdown sequence + } +} + ``` + + +### [4] server/index.ts + +`server/index.ts` is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point: + +### [5] server/plugin.ts + +`server/plugin.ts` is the server-side plugin definition. The shape of this plugin is the same as it’s client-side counter-part: + +```ts +import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; + +export class DemoPlugin implements Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup) { + // called when plugin is setting up during Kibana's startup sequence + } + + public start(core: CoreStart) { + // called after all plugins are set up + } + + public stop() { + // called when plugin is torn down during Kibana's shutdown sequence + } +} +``` + +Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain +considerations related to how plugins integrate with core APIs and APIs exposed by other plugins that may greatly impact how they are built. + +### [6] common/index.ts + +`common/index.ts` is the entry-point into code that can be used both server-side or client side. + +## How plugin's interact with each other, and Core + +The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. +For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler, +a plugin just accesses it off of the first argument: + +```ts +import type { CoreSetup } from 'kibana/server'; + +export class DemoPlugin { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + // handler is called when '/path' resource is requested with `GET` method + router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); + } +} +``` + +Unlike core, capabilities exposed by plugins are not automatically injected into all plugins. +Instead, if a plugin wishes to use the public interface provided by another plugin, it must first declare that plugin as a + dependency in it’s kibana.json manifest file. + +** foobar plugin.ts: ** + +``` +import type { Plugin } from 'kibana/server'; +export interface FoobarPluginSetup { [1] + getFoo(): string; +} + +export interface FoobarPluginStart { [1] + getBar(): string; +} + +export class MyPlugin implements Plugin { + public setup(): FoobarPluginSetup { + return { + getFoo() { + return 'foo'; + }, + }; + } + + public start(): FoobarPluginStart { + return { + getBar() { + return 'bar'; + }, + }; + } +} +``` +[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins. + + +** demo kibana.json** + +``` +{ + "id": "demo", + "requiredPlugins": ["foobar"], + "server": true, + "ui": true +} +``` + +With that specified in the plugin manifest, the appropriate interfaces are then available via the second argument of setup and/or start: + +```ts +import type { CoreSetup, CoreStart } from 'kibana/server'; +import type { FoobarPluginSetup, FoobarPluginStart } from '../../foobar/server'; + +interface DemoSetupPlugins { [1] + foobar: FoobarPluginSetup; +} + +interface DemoStartPlugins { + foobar: FoobarPluginStart; +} + +export class DemoPlugin { + public setup(core: CoreSetup, plugins: DemoSetupPlugins) { [2] + const { foobar } = plugins; + foobar.getFoo(); // 'foo' + foobar.getBar(); // throws because getBar does not exist + } + + public start(core: CoreStart, plugins: DemoStartPlugins) { [3] + const { foobar } = plugins; + foobar.getFoo(); // throws because getFoo does not exist + foobar.getBar(); // 'bar' + } + + public stop() {} +} +``` + +[1] The interface for plugin’s dependencies must be manually composed. You can do this by importing the appropriate type from the plugin and constructing an interface where the property name is the plugin’s ID. + +[2] These manually constructed types should then be used to specify the type of the second argument to the plugin. + +[3] Notice that the type for the setup and start lifecycles are different. Plugin lifecycle functions can only access the APIs that are exposed during that lifecycle. From 6c224e5a107f57f990bf74331ada027e0482e8a4 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 9 Feb 2021 14:34:34 -0700 Subject: [PATCH 078/100] Switch import route tag from access:ml:canFindFileStructure to access:fileUpload:import (#90677) --- x-pack/plugins/file_upload/server/routes.ts | 2 +- x-pack/plugins/maps/server/plugin.ts | 1 + x-pack/plugins/maps_file_upload/server/routes/file_upload.js | 1 + x-pack/plugins/ml/common/types/capabilities.ts | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/file_upload/server/routes.ts b/x-pack/plugins/file_upload/server/routes.ts index 425e5551f2147..d7b7b8f99edd9 100644 --- a/x-pack/plugins/file_upload/server/routes.ts +++ b/x-pack/plugins/file_upload/server/routes.ts @@ -52,7 +52,7 @@ export function fileUploadRoutes(router: IRouter) { accepts: ['application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, - tags: ['access:ml:canFindFileStructure'], + tags: ['access:fileUpload:import'], }, }, async (context, request, response) => { diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 7440b6ee1e1df..cb22a98b70aa8 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -177,6 +177,7 @@ export class MapsPlugin implements Plugin { catalogue: [APP_ID], privileges: { all: { + api: ['fileUpload:import'], app: [APP_ID, 'kibana'], catalogue: [APP_ID], savedObject: { diff --git a/x-pack/plugins/maps_file_upload/server/routes/file_upload.js b/x-pack/plugins/maps_file_upload/server/routes/file_upload.js index 1b617c44113a2..517fab13363a2 100644 --- a/x-pack/plugins/maps_file_upload/server/routes/file_upload.js +++ b/x-pack/plugins/maps_file_upload/server/routes/file_upload.js @@ -41,6 +41,7 @@ const options = { maxBytes: MAX_BYTES, accepts: ['application/json'], }, + tags: ['access:fileUpload:import'], }; export const idConditionalValidation = (body, boolHasId) => diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index eb7615c79a363..974a1f2243060 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -99,7 +99,7 @@ export function getPluginPrivileges() { return { admin: { ...privilege, - api: allMlCapabilitiesKeys.map((k) => `ml:${k}`), + api: ['fileUpload:import', ...allMlCapabilitiesKeys.map((k) => `ml:${k}`)], catalogue: [PLUGIN_ID, `${PLUGIN_ID}_file_data_visualizer`], ui: allMlCapabilitiesKeys, savedObject: { From 08111e40d32583382ae6e6a300f014cef7d18eba Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 9 Feb 2021 22:41:47 +0100 Subject: [PATCH 079/100] [Uptime-UX] Added nav search keywords for uptime and user experience app (#90616) --- x-pack/plugins/apm/public/plugin.ts | 19 +++++++++++++++++- x-pack/plugins/uptime/public/apps/plugin.ts | 22 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index f585515f79b0c..a1b3af6a9f943 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -162,7 +162,24 @@ export class ApmPlugin implements Plugin { order: 8500, euiIconType: 'logoObservability', category: DEFAULT_APP_CATEGORIES.observability, - + meta: { + keywords: [ + 'RUM', + 'Real User Monitoring', + 'DEM', + 'Digital Experience Monitoring', + 'EUM', + 'End User Monitoring', + 'UX', + 'Javascript', + 'APM', + 'Mobile', + 'digital', + 'performance', + 'web performance', + 'web perf', + ], + }, async mount(params: AppMountParameters) { // Load application bundle and Get start service const [{ renderApp }, [coreStart, corePlugins]] = await Promise.all([ diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index 8bbbecf8108fe..e7a22a080d79a 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -87,6 +87,28 @@ export class UptimePlugin order: 8400, title: PLUGIN.TITLE, category: DEFAULT_APP_CATEGORIES.observability, + meta: { + keywords: [ + 'Synthetics', + 'pings', + 'checks', + 'availability', + 'response duration', + 'response time', + 'outside in', + 'reachability', + 'reachable', + 'digital', + 'performance', + 'web performance', + 'web perf', + ], + searchDeepLinks: [ + { id: 'Down monitors', title: 'Down monitors', path: '/?statusFilter=down' }, + { id: 'Certificates', title: 'TLS Certificates', path: '/certificates' }, + { id: 'Settings', title: 'Settings', path: '/settings' }, + ], + }, mount: async (params: AppMountParameters) => { const [coreStart, corePlugins] = await core.getStartServices(); From 1a9f8cab1775419df669474fee233e51582cb5cf Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Tue, 9 Feb 2021 14:12:46 -0800 Subject: [PATCH 080/100] [uptime] unskip previously skipped functional tests (#90702) * fixes https://github.com/elastic/kibana/issues/74449 * unskipping uptime -functional test * unskipping location.ts script --- x-pack/test/functional/apps/uptime/locations.ts | 3 +-- x-pack/test/functional/apps/uptime/ping_redirects.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/locations.ts b/x-pack/test/functional/apps/uptime/locations.ts index e3f1d04640754..15b4773373bf7 100644 --- a/x-pack/test/functional/apps/uptime/locations.ts +++ b/x-pack/test/functional/apps/uptime/locations.ts @@ -39,8 +39,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await makeChecksWithStatus(es, LessAvailMonitor, 5, 2, 10000, {}, 'down'); }; - // FLAKY: https://github.com/elastic/kibana/issues/85208 - describe.skip('Observer location', () => { + describe('Observer location', () => { const start = '~ 15 minutes ago'; const end = 'now'; diff --git a/x-pack/test/functional/apps/uptime/ping_redirects.ts b/x-pack/test/functional/apps/uptime/ping_redirects.ts index e0abee38f4195..9c39ed7017721 100644 --- a/x-pack/test/functional/apps/uptime/ping_redirects.ts +++ b/x-pack/test/functional/apps/uptime/ping_redirects.ts @@ -19,8 +19,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const monitor = () => uptime.monitor; - // FLAKY: https://github.com/elastic/kibana/issues/84992 - describe.skip('Ping redirects', () => { + describe('Ping redirects', () => { const start = '~ 15 minutes ago'; const end = 'now'; From a9ee83c37c8b5171fdbf03ff20fc0e0788b81eab Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 9 Feb 2021 15:49:44 -0700 Subject: [PATCH 081/100] [Maps] convert HeatmapLayer to TS (#90666) * [Maps] convert HeatmapLayer to TS * make VectorLayer implement IVectorLayer * fix jest test and functional test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../layer_descriptor_types.ts | 10 +- .../public/actions/data_request_actions.ts | 2 +- .../maps/public/actions/layer_actions.ts | 2 +- .../public/classes/joins/inner_join.test.js | 2 +- .../blended_vector_layer.ts | 3 +- .../create_choropleth_layer_descriptor.ts | 2 +- .../create_region_map_layer_descriptor.ts | 3 +- .../create_tile_map_layer_descriptor.ts | 6 +- .../layers/file_upload_wizard/wizard.tsx | 2 +- .../layers/heatmap_layer/heatmap_layer.js | 122 ------------ .../layers/heatmap_layer/heatmap_layer.ts | 182 ++++++++++++++++++ .../classes/layers/heatmap_layer/index.ts | 8 + .../maps/public/classes/layers/layer.tsx | 7 + .../classes/layers/load_layer_wizards.ts | 1 - .../create_layer_descriptor.test.ts | 1 - .../observability/create_layer_descriptor.ts | 6 +- .../security/create_layer_descriptors.ts | 2 +- .../tiled_vector_layer/tiled_vector_layer.tsx | 2 +- .../vector_layer}/assign_feature_ids.test.ts | 2 +- .../vector_layer}/assign_feature_ids.ts | 2 +- .../classes/layers/vector_layer/index.ts | 9 + .../classes/layers/vector_layer/utils.tsx | 114 +++++++++++ .../layers/vector_layer/vector_layer.tsx | 126 ++---------- .../ems_boundaries_layer_wizard.tsx | 2 +- .../ems_file_source/ems_file_source.test.tsx | 2 +- .../clusters_layer_wizard.tsx | 3 +- .../heatmap_layer_wizard.tsx | 4 +- .../es_geo_grid_source/{index.js => index.ts} | 0 .../es_geo_line_source/layer_wizard.tsx | 2 +- .../point_2_point_layer_wizard.tsx | 2 +- .../create_layer_descriptor.ts | 2 +- .../es_documents_layer_wizard.tsx | 2 +- .../es_term_source/es_term_source.test.js | 2 +- .../kibana_regionmap_layer_wizard.tsx | 2 +- .../classes/styles/heatmap/heatmap_style.tsx | 2 +- .../components/vector_style_editor.test.tsx | 2 +- .../vector/components/vector_style_editor.tsx | 2 +- .../dynamic_color_property.test.tsx | 2 +- .../properties/dynamic_icon_property.test.tsx | 2 +- .../properties/dynamic_size_property.test.tsx | 2 +- .../properties/dynamic_size_property.tsx | 2 +- .../properties/dynamic_style_property.tsx | 2 +- .../classes/styles/vector/vector_style.tsx | 2 +- .../public/selectors/map_selectors.test.ts | 4 +- .../maps/public/selectors/map_selectors.ts | 12 +- 45 files changed, 385 insertions(+), 288 deletions(-) delete mode 100644 x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js create mode 100644 x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts create mode 100644 x-pack/plugins/maps/public/classes/layers/heatmap_layer/index.ts rename x-pack/plugins/maps/public/classes/{util => layers/vector_layer}/assign_feature_ids.test.ts (97%) rename x-pack/plugins/maps/public/classes/{util => layers/vector_layer}/assign_feature_ids.ts (96%) create mode 100644 x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts create mode 100644 x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx rename x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/{index.js => index.ts} (100%) diff --git a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts index 62de8baa66d5f..7a21599605b52 100644 --- a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts @@ -8,7 +8,11 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Query } from 'src/plugins/data/public'; -import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types'; +import { + HeatmapStyleDescriptor, + StyleDescriptor, + VectorStyleDescriptor, +} from './style_property_descriptor_types'; import { DataRequestDescriptor } from './data_request_descriptor_types'; import { AbstractSourceDescriptor, TermJoinSourceDescriptor } from './source_descriptor_types'; @@ -40,3 +44,7 @@ export type LayerDescriptor = { export type VectorLayerDescriptor = LayerDescriptor & { style: VectorStyleDescriptor; }; + +export type HeatmapLayerDescriptor = LayerDescriptor & { + style: HeatmapStyleDescriptor; +}; diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index 2e6a8098e5c21..5e8a18348ac5a 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -40,7 +40,7 @@ import { UPDATE_SOURCE_DATA_REQUEST, } from './map_action_constants'; import { ILayer } from '../classes/layers/layer'; -import { IVectorLayer } from '../classes/layers/vector_layer/vector_layer'; +import { IVectorLayer } from '../classes/layers/vector_layer'; import { DataMeta, MapExtent, MapFilters } from '../../common/descriptor_types'; import { DataRequestAbortError } from '../classes/util/data_request'; import { scaleBounds, turfBboxToBounds } from '../../common/elasticsearch_util'; diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index 16aa44af4460f..d68e4744975f1 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -42,7 +42,7 @@ import { clearDataRequests, syncDataForLayerId, updateStyleMeta } from './data_r import { cleanTooltipStateForLayer } from './tooltip_actions'; import { JoinDescriptor, LayerDescriptor, StyleDescriptor } from '../../common/descriptor_types'; import { ILayer } from '../classes/layers/layer'; -import { IVectorLayer } from '../classes/layers/vector_layer/vector_layer'; +import { IVectorLayer } from '../classes/layers/vector_layer'; import { LAYER_STYLE_TYPE, LAYER_TYPE } from '../../common/constants'; import { IVectorStyle } from '../classes/styles/vector/vector_style'; import { notifyLicensedFeatureUsage } from '../licensed_features'; diff --git a/x-pack/plugins/maps/public/classes/joins/inner_join.test.js b/x-pack/plugins/maps/public/classes/joins/inner_join.test.js index 749745530af7e..67fbf94fd1787 100644 --- a/x-pack/plugins/maps/public/classes/joins/inner_join.test.js +++ b/x-pack/plugins/maps/public/classes/joins/inner_join.test.js @@ -9,7 +9,7 @@ import { InnerJoin } from './inner_join'; import { SOURCE_TYPES } from '../../../common/constants'; jest.mock('../../kibana_services', () => {}); -jest.mock('../layers/vector_layer/vector_layer', () => {}); +jest.mock('../layers/vector_layer', () => {}); const rightSource = { type: SOURCE_TYPES.ES_TERM_SOURCE, diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 5d4b915c4e971..d3a4fa4101ac9 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { VectorLayer } from '../vector_layer/vector_layer'; +import { IVectorLayer, VectorLayer } from '../vector_layer'; import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; @@ -24,7 +24,6 @@ import { } from '../../../../common/constants'; import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; -import { IVectorLayer } from '../vector_layer/vector_layer'; import { IESSource } from '../../sources/es_source'; import { ISource } from '../../sources/source'; import { DataRequestContext } from '../../../actions'; diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts index a85ba041c4351..a4955a965d77c 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts @@ -23,7 +23,7 @@ import { ESSearchSourceDescriptor, } from '../../../../common/descriptor_types'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { VectorLayer } from '../vector_layer/vector_layer'; +import { VectorLayer } from '../vector_layer'; import { EMSFileSource } from '../../sources/ems_file_source'; // @ts-ignore import { ESSearchSource } from '../../sources/es_search_source'; diff --git a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts index 8e0d234445355..658a093321500 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts @@ -22,8 +22,7 @@ import { } from '../../../common/constants'; import { VectorStyle } from '../styles/vector/vector_style'; import { EMSFileSource } from '../sources/ems_file_source'; -// @ts-ignore -import { VectorLayer } from './vector_layer/vector_layer'; +import { VectorLayer } from './vector_layer'; import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes'; import { getJoinAggKey } from '../../../common/get_agg_key'; diff --git a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts index a9de8c98ee557..e3e5f3878ee56 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts @@ -23,11 +23,9 @@ import { VECTOR_STYLES, } from '../../../common/constants'; import { VectorStyle } from '../styles/vector/vector_style'; -// @ts-ignore import { ESGeoGridSource } from '../sources/es_geo_grid_source'; -import { VectorLayer } from './vector_layer/vector_layer'; -// @ts-ignore -import { HeatmapLayer } from './heatmap_layer/heatmap_layer'; +import { VectorLayer } from './vector_layer'; +import { HeatmapLayer } from './heatmap_layer'; import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes'; import { getSourceAggKey } from '../../../common/get_agg_key'; diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index a61ea4ce713a8..71be12c4304a6 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -16,7 +16,7 @@ import { } from '../../../../common/constants'; import { getFileUploadComponent } from '../../../kibana_services'; import { GeoJsonFileSource } from '../../sources/geojson_file_source'; -import { VectorLayer } from '../../layers/vector_layer/vector_layer'; +import { VectorLayer } from '../../layers/vector_layer'; import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { FileUploadComponentProps } from '../../../../../maps_file_upload/public'; diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js deleted file mode 100644 index 97cc7151112bf..0000000000000 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js +++ /dev/null @@ -1,122 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AbstractLayer } from '../layer'; -import { VectorLayer } from '../vector_layer/vector_layer'; -import { HeatmapStyle } from '../../styles/heatmap/heatmap_style'; -import { EMPTY_FEATURE_COLLECTION, LAYER_TYPE } from '../../../../common/constants'; - -const SCALED_PROPERTY_NAME = '__kbn_heatmap_weight__'; //unique name to store scaled value for weighting - -export class HeatmapLayer extends VectorLayer { - static type = LAYER_TYPE.HEATMAP; - - static createDescriptor(options) { - const heatmapLayerDescriptor = super.createDescriptor(options); - heatmapLayerDescriptor.type = HeatmapLayer.type; - heatmapLayerDescriptor.style = HeatmapStyle.createDescriptor(); - return heatmapLayerDescriptor; - } - - constructor({ layerDescriptor, source }) { - super({ layerDescriptor, source }); - if (!layerDescriptor.style) { - const defaultStyle = HeatmapStyle.createDescriptor(); - this._style = new HeatmapStyle(defaultStyle); - } else { - this._style = new HeatmapStyle(layerDescriptor.style); - } - } - - getStyleForEditing() { - return this._style; - } - - getStyle() { - return this._style; - } - - getCurrentStyle() { - return this._style; - } - - _getPropKeyOfSelectedMetric() { - const metricfields = this.getSource().getMetricFields(); - return metricfields[0].getName(); - } - - _getHeatmapLayerId() { - return this.makeMbLayerId('heatmap'); - } - - getMbLayerIds() { - return [this._getHeatmapLayerId()]; - } - - ownsMbLayerId(mbLayerId) { - return this._getHeatmapLayerId() === mbLayerId; - } - - syncLayerWithMB(mbMap) { - super._syncSourceBindingWithMb(mbMap); - - const heatmapLayerId = this._getHeatmapLayerId(); - if (!mbMap.getLayer(heatmapLayerId)) { - mbMap.addLayer({ - id: heatmapLayerId, - type: 'heatmap', - source: this.getId(), - paint: {}, - }); - } - - const mbSourceAfter = mbMap.getSource(this.getId()); - const sourceDataRequest = this.getSourceDataRequest(); - const featureCollection = sourceDataRequest ? sourceDataRequest.getData() : null; - if (!featureCollection) { - mbSourceAfter.setData(EMPTY_FEATURE_COLLECTION); - return; - } - - const propertyKey = this._getPropKeyOfSelectedMetric(); - const dataBoundToMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId()); - if (featureCollection !== dataBoundToMap) { - let max = 1; //max will be at least one, since counts or sums will be at least one. - for (let i = 0; i < featureCollection.features.length; i++) { - max = Math.max(featureCollection.features[i].properties[propertyKey], max); - } - for (let i = 0; i < featureCollection.features.length; i++) { - featureCollection.features[i].properties[SCALED_PROPERTY_NAME] = - featureCollection.features[i].properties[propertyKey] / max; - } - mbSourceAfter.setData(featureCollection); - } - - this.syncVisibilityWithMb(mbMap, heatmapLayerId); - this.getCurrentStyle().setMBPaintProperties({ - mbMap, - layerId: heatmapLayerId, - propertyName: SCALED_PROPERTY_NAME, - resolution: this.getSource().getGridResolution(), - }); - mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha()); - mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom()); - } - - getLayerTypeIconName() { - return 'heatmap'; - } - - async hasLegendDetails() { - return true; - } - - renderLegendDetails() { - const metricFields = this.getSource().getMetricFields(); - return this.getCurrentStyle().renderLegendDetails(metricFields[0]); - } -} diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts new file mode 100644 index 0000000000000..8eebd7c57afd7 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Map as MbMap, GeoJSONSource as MbGeoJSONSource } from 'mapbox-gl'; +import { FeatureCollection } from 'geojson'; +import { AbstractLayer } from '../layer'; +import { HeatmapStyle } from '../../styles/heatmap/heatmap_style'; +import { EMPTY_FEATURE_COLLECTION, LAYER_TYPE } from '../../../../common/constants'; +import { HeatmapLayerDescriptor, MapQuery } from '../../../../common/descriptor_types'; +import { ESGeoGridSource } from '../../sources/es_geo_grid_source'; +import { addGeoJsonMbSource, syncVectorSource } from '../vector_layer'; +import { DataRequestContext } from '../../../actions'; +import { DataRequestAbortError } from '../../util/data_request'; + +const SCALED_PROPERTY_NAME = '__kbn_heatmap_weight__'; // unique name to store scaled value for weighting + +export class HeatmapLayer extends AbstractLayer { + static type = LAYER_TYPE.HEATMAP; + + private readonly _style: HeatmapStyle; + + static createDescriptor(options: Partial) { + const heatmapLayerDescriptor = super.createDescriptor(options); + heatmapLayerDescriptor.type = HeatmapLayer.type; + heatmapLayerDescriptor.style = HeatmapStyle.createDescriptor(); + return heatmapLayerDescriptor; + } + + constructor({ + layerDescriptor, + source, + }: { + layerDescriptor: HeatmapLayerDescriptor; + source: ESGeoGridSource; + }) { + super({ layerDescriptor, source }); + if (!layerDescriptor.style) { + const defaultStyle = HeatmapStyle.createDescriptor(); + this._style = new HeatmapStyle(defaultStyle); + } else { + this._style = new HeatmapStyle(layerDescriptor.style); + } + } + + getSource(): ESGeoGridSource { + return super.getSource() as ESGeoGridSource; + } + + getStyleForEditing() { + return this._style; + } + + getStyle() { + return this._style; + } + + getCurrentStyle() { + return this._style; + } + + _getPropKeyOfSelectedMetric() { + const metricfields = this.getSource().getMetricFields(); + return metricfields[0].getName(); + } + + _getHeatmapLayerId() { + return this.makeMbLayerId('heatmap'); + } + + getMbLayerIds() { + return [this._getHeatmapLayerId()]; + } + + ownsMbLayerId(mbLayerId: string) { + return this._getHeatmapLayerId() === mbLayerId; + } + + ownsMbSourceId(mbSourceId: string) { + return this.getId() === mbSourceId; + } + + async syncData(syncContext: DataRequestContext) { + if (this.isLoadingBounds()) { + return; + } + + const sourceQuery = this.getQuery() as MapQuery; + try { + await syncVectorSource({ + layerId: this.getId(), + layerName: await this.getDisplayName(this.getSource()), + prevDataRequest: this.getSourceDataRequest(), + requestMeta: { + ...syncContext.dataFilters, + fieldNames: this.getSource().getFieldNames(), + geogridPrecision: this.getSource().getGeoGridPrecision(syncContext.dataFilters.zoom), + sourceQuery: sourceQuery ? sourceQuery : undefined, + applyGlobalQuery: this.getSource().getApplyGlobalQuery(), + applyGlobalTime: this.getSource().getApplyGlobalTime(), + sourceMeta: this.getSource().getSyncMeta(), + }, + syncContext, + source: this.getSource(), + }); + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + throw error; + } + } + } + + syncLayerWithMB(mbMap: MbMap) { + addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap); + + const heatmapLayerId = this._getHeatmapLayerId(); + if (!mbMap.getLayer(heatmapLayerId)) { + mbMap.addLayer({ + id: heatmapLayerId, + type: 'heatmap', + source: this.getId(), + paint: {}, + }); + } + + const mbGeoJSONSource = mbMap.getSource(this.getId()) as MbGeoJSONSource; + const sourceDataRequest = this.getSourceDataRequest(); + const featureCollection = sourceDataRequest + ? (sourceDataRequest.getData() as FeatureCollection) + : null; + if (!featureCollection) { + mbGeoJSONSource.setData(EMPTY_FEATURE_COLLECTION); + return; + } + + const propertyKey = this._getPropKeyOfSelectedMetric(); + const dataBoundToMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId()); + if (featureCollection !== dataBoundToMap) { + let max = 1; // max will be at least one, since counts or sums will be at least one. + for (let i = 0; i < featureCollection.features.length; i++) { + max = Math.max(featureCollection.features[i].properties?.[propertyKey], max); + } + for (let i = 0; i < featureCollection.features.length; i++) { + if (featureCollection.features[i].properties) { + featureCollection.features[i].properties![SCALED_PROPERTY_NAME] = + featureCollection.features[i].properties![propertyKey] / max; + } + } + mbGeoJSONSource.setData(featureCollection); + } + + this.syncVisibilityWithMb(mbMap, heatmapLayerId); + this.getCurrentStyle().setMBPaintProperties({ + mbMap, + layerId: heatmapLayerId, + propertyName: SCALED_PROPERTY_NAME, + resolution: this.getSource().getGridResolution(), + }); + mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha()); + mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom()); + } + + getLayerTypeIconName() { + return 'heatmap'; + } + + async getFields() { + return this.getSource().getFields(); + } + + async hasLegendDetails() { + return true; + } + + renderLegendDetails() { + const metricFields = this.getSource().getMetricFields(); + return this.getCurrentStyle().renderLegendDetails(metricFields[0]); + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/index.ts new file mode 100644 index 0000000000000..ba15d97a39219 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { HeatmapLayer } from './heatmap_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index aedf7af08b2c8..89c6d70a217c9 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -21,6 +21,7 @@ import { MAX_ZOOM, MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, MIN_ZOOM, + SOURCE_BOUNDS_DATA_REQUEST_ID, SOURCE_DATA_REQUEST_ID, SOURCE_TYPES, STYLE_TYPE, @@ -66,6 +67,7 @@ export interface ILayer { getImmutableSourceProperties(): Promise; renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null; isLayerLoading(): boolean; + isLoadingBounds(): boolean; isFilteredByGlobalTime(): Promise; hasErrors(): boolean; getErrors(): string; @@ -401,6 +403,11 @@ export class AbstractLayer implements ILayer { return this._dataRequests.some((dataRequest) => dataRequest.isLoading()); } + isLoadingBounds() { + const boundsDataRequest = this.getDataRequest(SOURCE_BOUNDS_DATA_REQUEST_ID); + return !!boundsDataRequest && boundsDataRequest.isLoading(); + } + hasErrors(): boolean { return _.get(this._descriptor, '__isInErrorState', false); } diff --git a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts index a32ae15405fac..bed7599f89073 100644 --- a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts +++ b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts @@ -9,7 +9,6 @@ import { registerLayerWizard } from './layer_wizard_registry'; import { uploadLayerWizardConfig } from './file_upload_wizard'; // @ts-ignore import { esDocumentsLayerWizardConfig } from '../sources/es_search_source'; -// @ts-ignore import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from '../sources/es_geo_grid_source'; import { geoLineLayerWizardConfig } from '../sources/es_geo_line_source'; // @ts-ignore diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts index c312ddec42572..b9cfb0067abd2 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts @@ -176,7 +176,6 @@ describe('createLayerDescriptor', () => { __dataRequests: [], alpha: 0.75, id: '12345', - joins: [], label: '[Performance] Duration', maxZoom: 24, minZoom: 0, diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts index fd9147d62cc26..03870e7668189 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts @@ -31,11 +31,9 @@ import { OBSERVABILITY_METRIC_TYPE } from './metric_select'; import { DISPLAY } from './display_select'; import { VectorStyle } from '../../../styles/vector/vector_style'; import { EMSFileSource } from '../../../sources/ems_file_source'; -// @ts-ignore import { ESGeoGridSource } from '../../../sources/es_geo_grid_source'; -import { VectorLayer } from '../../vector_layer/vector_layer'; -// @ts-ignore -import { HeatmapLayer } from '../../heatmap_layer/heatmap_layer'; +import { VectorLayer } from '../../vector_layer'; +import { HeatmapLayer } from '../../heatmap_layer'; import { getDefaultDynamicProperties } from '../../../styles/vector/vector_style_defaults'; // redefining APM constant to avoid making maps app depend on APM plugin diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts index 74a66276459c7..b2283196a41dd 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts @@ -22,7 +22,7 @@ import { SYMBOLIZE_AS_TYPES, VECTOR_STYLES, } from '../../../../../common/constants'; -import { VectorLayer } from '../../vector_layer/vector_layer'; +import { VectorLayer } from '../../vector_layer'; import { VectorStyle } from '../../../styles/vector/vector_style'; // @ts-ignore import { ESSearchSource } from '../../../sources/es_search_source'; diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index d98396b960cbd..477b17ae03d7b 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -15,7 +15,7 @@ import { EuiIcon } from '@elastic/eui'; import { Feature } from 'geojson'; import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE } from '../../../../common/constants'; -import { VectorLayer, VectorLayerArguments } from '../vector_layer/vector_layer'; +import { VectorLayer, VectorLayerArguments } from '../vector_layer'; import { ITiledSingleLayerVectorSource } from '../../sources/vector_source'; import { DataRequestContext } from '../../../actions'; import { diff --git a/x-pack/plugins/maps/public/classes/util/assign_feature_ids.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts similarity index 97% rename from x-pack/plugins/maps/public/classes/util/assign_feature_ids.test.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts index 4fe3804968b81..137d443b39b91 100644 --- a/x-pack/plugins/maps/public/classes/util/assign_feature_ids.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts @@ -6,7 +6,7 @@ */ import { assignFeatureIds } from './assign_feature_ids'; -import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants'; +import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; import { FeatureCollection, Feature, Point } from 'geojson'; const featureId = 'myFeature1'; diff --git a/x-pack/plugins/maps/public/classes/util/assign_feature_ids.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts similarity index 96% rename from x-pack/plugins/maps/public/classes/util/assign_feature_ids.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts index f6b7851159586..c40c8299ad04c 100644 --- a/x-pack/plugins/maps/public/classes/util/assign_feature_ids.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { FeatureCollection, Feature } from 'geojson'; -import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants'; +import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; let idCounter = 0; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts new file mode 100644 index 0000000000000..4b509ba5dff00 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { addGeoJsonMbSource, syncVectorSource } from './utils'; +export { IVectorLayer, VectorLayer, VectorLayerArguments } from './vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx new file mode 100644 index 0000000000000..a3754b20de818 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FeatureCollection } from 'geojson'; +import { Map as MbMap } from 'mapbox-gl'; +import { + EMPTY_FEATURE_COLLECTION, + SOURCE_DATA_REQUEST_ID, + VECTOR_SHAPE_TYPE, +} from '../../../../common/constants'; +import { VectorSourceRequestMeta } from '../../../../common/descriptor_types'; +import { DataRequestContext } from '../../../actions'; +import { IVectorSource } from '../../sources/vector_source'; +import { DataRequestAbortError } from '../../util/data_request'; +import { DataRequest } from '../../util/data_request'; +import { getCentroidFeatures } from '../../../../common/get_centroid_features'; +import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; +import { assignFeatureIds } from './assign_feature_ids'; + +export function addGeoJsonMbSource(mbSourceId: string, mbLayerIds: string[], mbMap: MbMap) { + const mbSource = mbMap.getSource(mbSourceId); + if (!mbSource) { + mbMap.addSource(mbSourceId, { + type: 'geojson', + data: EMPTY_FEATURE_COLLECTION, + }); + } else if (mbSource.type !== 'geojson') { + // Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer. + mbLayerIds.forEach((mbLayerId) => { + if (mbMap.getLayer(mbLayerId)) { + mbMap.removeLayer(mbLayerId); + } + }); + + mbMap.removeSource(mbSourceId); + mbMap.addSource(mbSourceId, { + type: 'geojson', + data: EMPTY_FEATURE_COLLECTION, + }); + } +} + +export async function syncVectorSource({ + layerId, + layerName, + prevDataRequest, + requestMeta, + syncContext, + source, +}: { + layerId: string; + layerName: string; + prevDataRequest: DataRequest | undefined; + requestMeta: VectorSourceRequestMeta; + syncContext: DataRequestContext; + source: IVectorSource; +}): Promise<{ refreshed: boolean; featureCollection: FeatureCollection }> { + const { + startLoading, + stopLoading, + onLoadError, + registerCancelCallback, + isRequestStillActive, + } = syncContext; + const dataRequestId = SOURCE_DATA_REQUEST_ID; + const requestToken = Symbol(`${layerId}-${dataRequestId}`); + const canSkipFetch = await canSkipSourceUpdate({ + source, + prevDataRequest, + nextMeta: requestMeta, + }); + if (canSkipFetch) { + return { + refreshed: false, + featureCollection: prevDataRequest + ? (prevDataRequest.getData() as FeatureCollection) + : EMPTY_FEATURE_COLLECTION, + }; + } + + try { + startLoading(dataRequestId, requestToken, requestMeta); + const { data: sourceFeatureCollection, meta } = await source.getGeoJsonWithMeta( + layerName, + requestMeta, + registerCancelCallback.bind(null, requestToken), + () => { + return isRequestStillActive(dataRequestId, requestToken); + } + ); + const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection); + const supportedShapes = await source.getSupportedShapeTypes(); + if ( + supportedShapes.includes(VECTOR_SHAPE_TYPE.LINE) || + supportedShapes.includes(VECTOR_SHAPE_TYPE.POLYGON) + ) { + layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection)); + } + stopLoading(dataRequestId, requestToken, layerFeatureCollection, meta); + return { + refreshed: true, + featureCollection: layerFeatureCollection, + }; + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + onLoadError(dataRequestId, requestToken, error.message); + } + throw error; + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index e9c0cb29c7c17..7e87d99fd4f93 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -13,10 +13,8 @@ import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AbstractLayer } from '../layer'; import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; -import { getCentroidFeatures } from '../../../../common/get_centroid_features'; import { FEATURE_ID_PROPERTY_NAME, - SOURCE_DATA_REQUEST_ID, SOURCE_META_DATA_REQUEST_ID, SOURCE_FORMATTERS_DATA_REQUEST_ID, SOURCE_BOUNDS_DATA_REQUEST_ID, @@ -25,10 +23,8 @@ import { KBN_TOO_MANY_FEATURES_PROPERTY, LAYER_TYPE, FIELD_ORIGIN, - LAYER_STYLE_TYPE, KBN_TOO_MANY_FEATURES_IMAGE_ID, FieldFormatter, - VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; import { DataRequestAbortError } from '../../util/data_request'; @@ -37,7 +33,6 @@ import { canSkipStyleMetaUpdate, canSkipFormattersUpdate, } from '../../util/can_skip_fetch'; -import { assignFeatureIds } from '../../util/assign_feature_ids'; import { getFeatureCollectionBounds } from '../../util/get_feature_collection_bounds'; import { getCentroidFilterExpression, @@ -65,6 +60,7 @@ import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_st import { IESSource } from '../../sources/es_source'; import { PropertiesMap } from '../../../../common/elasticsearch_util'; import { ITermJoinSource } from '../../sources/term_join_source'; +import { addGeoJsonMbSource, syncVectorSource } from './utils'; interface SourceResult { refreshed: boolean; @@ -95,7 +91,7 @@ export interface IVectorLayer extends ILayer { hasJoins(): boolean; } -export class VectorLayer extends AbstractLayer { +export class VectorLayer extends AbstractLayer implements IVectorLayer { static type = LAYER_TYPE.VECTOR; protected readonly _style: IVectorStyle; @@ -288,11 +284,6 @@ export class VectorLayer extends AbstractLayer { return bounds; } - isLoadingBounds() { - const boundsDataRequest = this.getDataRequest(SOURCE_BOUNDS_DATA_REQUEST_ID); - return !!boundsDataRequest && boundsDataRequest.isLoading(); - } - async getLeftJoinFields() { return await this.getSource().getLeftJoinFields(); } @@ -420,11 +411,9 @@ export class VectorLayer extends AbstractLayer { source: IVectorSource, style: IVectorStyle ): VectorSourceRequestMeta { - const styleFieldNames = - style.getType() === LAYER_STYLE_TYPE.VECTOR ? style.getSourceFieldNames() : []; const fieldNames = [ ...source.getFieldNames(), - ...styleFieldNames, + ...style.getSourceFieldNames(), ...this.getValidJoins().map((join) => join.getLeftField().getName()), ]; @@ -485,82 +474,11 @@ export class VectorLayer extends AbstractLayer { } } - async _syncSource( - syncContext: DataRequestContext, - source: IVectorSource, - style: IVectorStyle - ): Promise { - const { - startLoading, - stopLoading, - onLoadError, - registerCancelCallback, - dataFilters, - isRequestStillActive, - } = syncContext; - const dataRequestId = SOURCE_DATA_REQUEST_ID; - const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); - const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( - dataFilters, - source, - style - ); - const prevDataRequest = this.getSourceDataRequest(); - const canSkipFetch = await canSkipSourceUpdate({ - source, - prevDataRequest, - nextMeta: searchFilters, - }); - if (canSkipFetch) { - return { - refreshed: false, - featureCollection: prevDataRequest - ? (prevDataRequest.getData() as FeatureCollection) - : EMPTY_FEATURE_COLLECTION, - }; - } - - try { - startLoading(dataRequestId, requestToken, searchFilters); - const layerName = await this.getDisplayName(source); - const { data: sourceFeatureCollection, meta } = await source.getGeoJsonWithMeta( - layerName, - searchFilters, - registerCancelCallback.bind(null, requestToken), - () => { - return isRequestStillActive(dataRequestId, requestToken); - } - ); - const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection); - const supportedShapes = await source.getSupportedShapeTypes(); - if ( - supportedShapes.includes(VECTOR_SHAPE_TYPE.LINE) || - supportedShapes.includes(VECTOR_SHAPE_TYPE.POLYGON) - ) { - layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection)); - } - stopLoading(dataRequestId, requestToken, layerFeatureCollection, meta); - return { - refreshed: true, - featureCollection: layerFeatureCollection, - }; - } catch (error) { - if (!(error instanceof DataRequestAbortError)) { - onLoadError(dataRequestId, requestToken, error.message); - } - throw error; - } - } - async _syncSourceStyleMeta( syncContext: DataRequestContext, source: IVectorSource, style: IVectorStyle ) { - if (this.getCurrentStyle().getType() !== LAYER_STYLE_TYPE.VECTOR) { - return; - } - const sourceQuery = this.getQuery() as MapQuery; return this._syncStyleMeta({ source, @@ -665,10 +583,6 @@ export class VectorLayer extends AbstractLayer { source: IVectorSource, style: IVectorStyle ) { - if (style.getType() !== LAYER_STYLE_TYPE.VECTOR) { - return; - } - return this._syncFormatters({ source, dataRequestId: SOURCE_FORMATTERS_DATA_REQUEST_ID, @@ -773,7 +687,14 @@ export class VectorLayer extends AbstractLayer { try { await this._syncSourceStyleMeta(syncContext, source, style); await this._syncSourceFormatters(syncContext, source, style); - const sourceResult = await this._syncSource(syncContext, source, style); + const sourceResult = await syncVectorSource({ + layerId: this.getId(), + layerName: await this.getDisplayName(source), + prevDataRequest: this.getSourceDataRequest(), + requestMeta: this._getSearchFilters(syncContext.dataFilters, source, style), + syncContext, + source, + }); if ( !sourceResult.featureCollection || !sourceResult.featureCollection.features.length || @@ -1061,31 +982,8 @@ export class VectorLayer extends AbstractLayer { this._setMbCentroidProperties(mbMap); } - _syncSourceBindingWithMb(mbMap: MbMap) { - const mbSource = mbMap.getSource(this._getMbSourceId()); - if (!mbSource) { - mbMap.addSource(this._getMbSourceId(), { - type: 'geojson', - data: EMPTY_FEATURE_COLLECTION, - }); - } else if (mbSource.type !== 'geojson') { - // Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer. - this.getMbLayerIds().forEach((mbLayerId) => { - if (mbMap.getLayer(mbLayerId)) { - mbMap.removeLayer(mbLayerId); - } - }); - - mbMap.removeSource(this._getMbSourceId()); - mbMap.addSource(this._getMbSourceId(), { - type: 'geojson', - data: EMPTY_FEATURE_COLLECTION, - }); - } - } - syncLayerWithMB(mbMap: MbMap) { - this._syncSourceBindingWithMb(mbMap); + addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap); this._syncFeatureCollectionWithMb(mbMap); this._syncStylePropertiesWithMb(mbMap); } diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index 3acc3c59e5930..d4cf4dbee7943 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VectorLayer } from '../../layers/vector_layer/vector_layer'; +import { VectorLayer } from '../../layers/vector_layer'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { EMSFileSource, getSourceTitle } from './ems_file_source'; diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx index 5a0a3ed8df596..e711fb900e39a 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx @@ -7,7 +7,7 @@ import { EMSFileSource } from './ems_file_source'; -jest.mock('../../layers/vector_layer/vector_layer', () => {}); +jest.mock('../../layers/vector_layer', () => {}); function makeEMSFileSource(tooltipProperties: string[]) { const emsFileSource = new EMSFileSource({ tooltipProperties }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index 8951b7b278459..36dd28cb5bbf1 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -9,10 +9,9 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; -// @ts-ignore import { ESGeoGridSource, clustersTitle } from './es_geo_grid_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; -import { VectorLayer } from '../../layers/vector_layer/vector_layer'; +import { VectorLayer } from '../../layers/vector_layer'; import { ESGeoGridSourceDescriptor, ColorDynamicOptions, diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx index 83a7e02383f77..8fc26f3593750 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -9,11 +9,9 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; -// @ts-ignore import { ESGeoGridSource, heatmapTitle } from './es_geo_grid_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; -// @ts-ignore -import { HeatmapLayer } from '../../layers/heatmap_layer/heatmap_layer'; +import { HeatmapLayer } from '../../layers/heatmap_layer'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; import { LAYER_WIZARD_CATEGORY, RENDER_AS } from '../../../../common/constants'; import { HeatmapLayerIcon } from '../../layers/icons/heatmap_layer_icon'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/index.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/index.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/index.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/index.ts diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx index 6a1dfc74271d8..8da7037a5a34c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx @@ -12,7 +12,7 @@ import { ESGeoLineSource, geoLineTitle, REQUIRES_GOLD_LICENSE_MSG } from './es_g import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { LAYER_WIZARD_CATEGORY, STYLE_TYPE, VECTOR_STYLES } from '../../../../common/constants'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { VectorLayer } from '../../layers/vector_layer/vector_layer'; +import { VectorLayer } from '../../layers/vector_layer'; import { getIsGoldPlus } from '../../../licensed_features'; import { TracksLayerIcon } from '../../layers/icons/tracks_layer_icon'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index 49b161711481c..c94c7859a85e7 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; -import { VectorLayer } from '../../layers/vector_layer/vector_layer'; +import { VectorLayer } from '../../layers/vector_layer'; // @ts-ignore import { ESPewPewSource, sourceTitle } from './es_pew_pew_source'; import { VectorStyle } from '../../styles/vector/vector_style'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts index 2734af5742dbb..41b4e8d7a318a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts @@ -9,7 +9,7 @@ import { Query } from 'src/plugins/data/public'; import { LayerDescriptor } from '../../../../common/descriptor_types'; import { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants'; import { ESSearchSource } from './es_search_source'; -import { VectorLayer } from '../../layers/vector_layer/vector_layer'; +import { VectorLayer } from '../../layers/vector_layer'; import { getIsGoldPlus } from '../../../licensed_features'; export interface CreateLayerDescriptorParams { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index d01ed459e3171..c0606b5f4aec6 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -13,7 +13,7 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re // @ts-ignore import { ESSearchSource, sourceTitle } from './es_search_source'; import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer'; -import { VectorLayer } from '../../layers/vector_layer/vector_layer'; +import { VectorLayer } from '../../layers/vector_layer'; import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants'; import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; import { DocumentsLayerIcon } from '../../layers/icons/documents_layer_icon'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js index a7994db286112..1f4a1ab7c9afa 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js @@ -7,7 +7,7 @@ import { ESTermSource, extractPropertiesMap } from './es_term_source'; -jest.mock('../../layers/vector_layer/vector_layer', () => {}); +jest.mock('../../layers/vector_layer', () => {}); const indexPatternTitle = 'myIndex'; const termFieldName = 'myTermField'; diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index b41f599ac3d75..907b80e6405a6 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; // @ts-ignore import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source'; -import { VectorLayer } from '../../layers/vector_layer/vector_layer'; +import { VectorLayer } from '../../layers/vector_layer'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; import { getKibanaRegionList } from '../../../meta'; diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx index f30040cf93b57..fe581a1807b28 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx @@ -31,7 +31,7 @@ export class HeatmapStyle implements IStyle { this._descriptor = HeatmapStyle.createDescriptor(descriptor.colorRampName); } - static createDescriptor(colorRampName: string) { + static createDescriptor(colorRampName?: string) { return { type: LAYER_STYLE_TYPE.HEATMAP, colorRampName: colorRampName ? colorRampName : DEFAULT_HEATMAP_COLOR_RAMP_NAME, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx index 3cfae4a836042..d0d3a7c2abe06 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { StyleProperties, VectorStyleEditor } from './vector_style_editor'; import { getDefaultStaticProperties } from '../vector_style_defaults'; -import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; +import { IVectorLayer } from '../../../layers/vector_layer'; import { IVectorSource } from '../../../sources/vector_source'; import { FIELD_ORIGIN, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx index b36f3a38e2783..91bcc2dc06859 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx @@ -49,7 +49,7 @@ import { SymbolizeAsProperty } from '../properties/symbolize_as_property'; import { LabelBorderSizeProperty } from '../properties/label_border_size_property'; import { StaticTextProperty } from '../properties/static_text_property'; import { StaticSizeProperty } from '../properties/static_size_property'; -import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; +import { IVectorLayer } from '../../../layers/vector_layer'; export interface StyleProperties { [key: string]: IStyleProperty; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx index 03b7ce17063c3..b7e0133881ee1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx @@ -24,7 +24,7 @@ import { } from '../../../../../common/constants'; import { mockField, MockLayer, MockStyle } from './test_helpers/test_util'; import { ColorDynamicOptions } from '../../../../../common/descriptor_types'; -import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; +import { IVectorLayer } from '../../../layers/vector_layer'; import { IField } from '../../../fields/field'; const makeProperty = (options: ColorDynamicOptions, style?: MockStyle, field?: IField) => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index fc4d495f1e40a..46339c5a4a20d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -20,7 +20,7 @@ import { DynamicIconProperty } from './dynamic_icon_property'; import { mockField, MockLayer } from './test_helpers/test_util'; import { IconDynamicOptions } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; -import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; +import { IVectorLayer } from '../../../layers/vector_layer'; const makeProperty = (options: Partial, field: IField = mockField) => { const defaultOptions: IconDynamicOptions = { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx index 40d72a357218f..64a3e0cf0e322 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx @@ -21,7 +21,7 @@ import { IField } from '../../../fields/field'; import { Map as MbMap } from 'mapbox-gl'; import { SizeDynamicOptions } from '../../../../../common/descriptor_types'; import { mockField, MockLayer, MockStyle } from './test_helpers/test_util'; -import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; +import { IVectorLayer } from '../../../layers/vector_layer'; export class MockMbMap { _paintPropertyCalls: unknown[]; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx index 52b78b4211a2d..7076775dcce31 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx @@ -20,7 +20,7 @@ import { import { FieldFormatter, MB_LOOKUP_FUNCTION, VECTOR_STYLES } from '../../../../../common/constants'; import { SizeDynamicOptions } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; -import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; +import { IVectorLayer } from '../../../layers/vector_layer'; export class DynamicSizeProperty extends DynamicStyleProperty { private readonly _isSymbolizedAsIcon: boolean; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index f62b17ee05ad6..9ffd9a0f1b345 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -34,7 +34,7 @@ import { StyleMetaData, } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; -import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; +import { IVectorLayer } from '../../../layers/vector_layer'; import { InnerJoin } from '../../../joins/inner_join'; import { IVectorStyle } from '../vector_style'; import { getComputedFieldName } from '../style_util'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index c61e72807224a..692be08d07bc6 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -70,7 +70,7 @@ import { DataRequest } from '../../util/data_request'; import { IStyle } from '../style'; import { IStyleProperty } from './properties/style_property'; import { IField } from '../../fields/field'; -import { IVectorLayer } from '../../layers/vector_layer/vector_layer'; +import { IVectorLayer } from '../../layers/vector_layer'; import { IVectorSource } from '../../sources/vector_source'; import { createStyleFieldsHelper, StyleFieldsHelper } from './style_fields_helper'; import { IESAggField } from '../../fields/agg'; diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts index dd6a9fc377e5b..c2f5fc02c5df2 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -jest.mock('../classes/layers/vector_layer/vector_layer', () => {}); +jest.mock('../classes/layers/vector_layer', () => {}); jest.mock('../classes/layers/tiled_vector_layer/tiled_vector_layer', () => {}); jest.mock('../classes/layers/blended_vector_layer/blended_vector_layer', () => {}); -jest.mock('../classes/layers/heatmap_layer/heatmap_layer', () => {}); +jest.mock('../classes/layers/heatmap_layer', () => {}); jest.mock('../classes/layers/vector_tile_layer/vector_tile_layer', () => {}); jest.mock('../classes/joins/inner_join', () => {}); jest.mock('../kibana_services', () => ({ diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 27281fe17f0fa..f53f39ad2fc0c 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -12,10 +12,9 @@ import { Adapters } from 'src/plugins/inspector/public'; import { TileLayer } from '../classes/layers/tile_layer/tile_layer'; // @ts-ignore import { VectorTileLayer } from '../classes/layers/vector_tile_layer/vector_tile_layer'; -import { IVectorLayer, VectorLayer } from '../classes/layers/vector_layer/vector_layer'; +import { IVectorLayer, VectorLayer } from '../classes/layers/vector_layer'; import { VectorStyle } from '../classes/styles/vector/vector_style'; -// @ts-ignore -import { HeatmapLayer } from '../classes/layers/heatmap_layer/heatmap_layer'; +import { HeatmapLayer } from '../classes/layers/heatmap_layer'; import { BlendedVectorLayer } from '../classes/layers/blended_vector_layer/blended_vector_layer'; import { getTimeFilter } from '../kibana_services'; import { @@ -41,6 +40,7 @@ import { DataRequestDescriptor, DrawState, Goto, + HeatmapLayerDescriptor, LayerDescriptor, MapCenter, MapExtent, @@ -54,6 +54,7 @@ import { Filter, TimeRange } from '../../../../../src/plugins/data/public'; import { ISource } from '../classes/sources/source'; import { ITMSSource } from '../classes/sources/tms_source'; import { IVectorSource } from '../classes/sources/vector_source'; +import { ESGeoGridSource } from '../classes/sources/es_geo_grid_source'; import { ILayer } from '../classes/layers/layer'; export function createLayerInstance( @@ -84,7 +85,10 @@ export function createLayerInstance( case VectorTileLayer.type: return new VectorTileLayer({ layerDescriptor, source: source as ITMSSource }); case HeatmapLayer.type: - return new HeatmapLayer({ layerDescriptor, source }); + return new HeatmapLayer({ + layerDescriptor: layerDescriptor as HeatmapLayerDescriptor, + source: source as ESGeoGridSource, + }); case BlendedVectorLayer.type: return new BlendedVectorLayer({ layerDescriptor: layerDescriptor as VectorLayerDescriptor, From d14e37fd685beec8318323746da998fadce61dc7 Mon Sep 17 00:00:00 2001 From: Nick Peihl Date: Tue, 9 Feb 2021 15:00:37 -0800 Subject: [PATCH 082/100] Bump @elastic/ems-client@7.12.0 (#90841) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7144745f2ae35..aac576dbc3561 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "@babel/runtime": "^7.12.5", "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", - "@elastic/ems-client": "7.11.0", + "@elastic/ems-client": "7.12.0", "@elastic/eui": "31.4.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", diff --git a/yarn.lock b/yarn.lock index ec6cf338a43da..c9f3186ffcba2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2187,10 +2187,10 @@ pump "^3.0.0" secure-json-parse "^2.1.0" -"@elastic/ems-client@7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.11.0.tgz#d2142d0ef5bd1aff7ae67b37c1394b73cdd48d8b" - integrity sha512-7+gDEkBr8nRS7X9i/UPg1WkS7bEBuNbBBjXCchQeYwqPRmw6vOb4wjlNzVwmOFsp2OH4lVFfZ+XU4pxTt32EXA== +"@elastic/ems-client@7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.12.0.tgz#cf83f5ad76e26cedfa6f5b91277d2d919b9423d1" + integrity sha512-Svv3boWL1n14nIt6tL9gaA9Ym1B4AwWl6ISZT62+uKM2G+imZxWLkqpQc/HHcf7TfuAmleF2NFwnT5vw2vZTpA== dependencies: lodash "^4.17.15" semver "7.3.2" From 8e4f8993800f4990c007191980975b71a9173abe Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 9 Feb 2021 17:21:44 -0600 Subject: [PATCH 083/100] Ignore intermediate records in ML Rule anomalies query (#90316) We were incorrectly including records with is_interim: true in our query, which lead to false positive signals if the rule executed while an anomaly's score was (temporarily) above the specified threshold, but then dipped below after it was finalized. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/machine_learning/index.test.ts | 21 +++++++++++++++++-- .../server/lib/machine_learning/index.ts | 1 + 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts index 86133d93e99c8..1f49ac7bf5019 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ESFilter } from '../../../../../typings/elasticsearch'; import { getExceptionListItemSchemaMock } from '../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getAnomalies, AnomaliesSearchParams } from '.'; @@ -13,8 +14,8 @@ const getFiltersFromMock = (mock: jest.Mock) => { return searchParams.body.query.bool.filter; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const getBoolCriteriaFromFilters = (filters: any[]) => filters[1].bool.must; +const getBoolCriteriaFromFilters = (filters: ESFilter[]) => + filters.find((filter) => filter?.bool?.must)?.bool?.must; describe('getAnomalies', () => { let searchParams: AnomaliesSearchParams; @@ -104,4 +105,20 @@ describe('getAnomalies', () => { ]) ); }); + + it('ignores anomalies that do not have finalized scores', () => { + const mockMlAnomalySearch = jest.fn(); + getAnomalies(searchParams, mockMlAnomalySearch); + const filters = getFiltersFromMock(mockMlAnomalySearch); + + expect(filters).toEqual( + expect.arrayContaining([ + { + term: { + is_interim: false, + }, + }, + ]) + ); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts index 962c44174d891..c3fea9f6d916f 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts @@ -47,6 +47,7 @@ export const getAnomalies = async ( analyze_wildcard: false, }, }, + { term: { is_interim: false } }, { bool: { must: boolCriteria, From 7627d35443a548037ec5cceb92763be9005e2590 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Tue, 9 Feb 2021 15:34:42 -0800 Subject: [PATCH 084/100] Getting rid of the x-pack tsconfig refs (#90537) --- src/dev/typescript/build_refs.ts | 2 -- tsconfig.refs.json | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/dev/typescript/build_refs.ts b/src/dev/typescript/build_refs.ts index ff6a81843972c..77d6eb2abc612 100644 --- a/src/dev/typescript/build_refs.ts +++ b/src/dev/typescript/build_refs.ts @@ -7,12 +7,10 @@ */ import execa from 'execa'; -import Path from 'path'; import { run, ToolingLog } from '@kbn/dev-utils'; export async function buildAllRefs(log: ToolingLog) { await buildRefs(log, 'tsconfig.refs.json'); - await buildRefs(log, Path.join('x-pack', 'tsconfig.refs.json')); } async function buildRefs(log: ToolingLog, projectPath: string) { diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 17b1fc5dc1fe9..86fdfad2f524d 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -55,5 +55,61 @@ { "path": "./src/plugins/visualizations/tsconfig.json" }, { "path": "./src/plugins/visualize/tsconfig.json" }, { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, + { "path": "./x-pack/plugins/actions/tsconfig.json" }, + { "path": "./x-pack/plugins/alerts/tsconfig.json" }, + { "path": "./x-pack/plugins/beats_management/tsconfig.json" }, + { "path": "./x-pack/plugins/canvas/tsconfig.json" }, + { "path": "./x-pack/plugins/cloud/tsconfig.json" }, + { "path": "./x-pack/plugins/code/tsconfig.json" }, + { "path": "./x-pack/plugins/console_extensions/tsconfig.json" }, + { "path": "./x-pack/plugins/dashboard_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/data_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/dashboard_mode/tsconfig.json" }, + { "path": "./x-pack/plugins/discover_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/encrypted_saved_objects/tsconfig.json" }, + { "path": "./x-pack/plugins/enterprise_search/tsconfig.json" }, + { "path": "./x-pack/plugins/event_log/tsconfig.json" }, + { "path": "./x-pack/plugins/features/tsconfig.json" }, + { "path": "./x-pack/plugins/file_upload/tsconfig.json" }, + { "path": "./x-pack/plugins/fleet/tsconfig.json" }, + { "path": "./x-pack/plugins/global_search_bar/tsconfig.json" }, + { "path": "./x-pack/plugins/global_search_providers/tsconfig.json" }, + { "path": "./x-pack/plugins/global_search/tsconfig.json" }, + { "path": "./x-pack/plugins/graph/tsconfig.json" }, + { "path": "./x-pack/plugins/grokdebugger/tsconfig.json" }, + { "path": "./x-pack/plugins/infra/tsconfig.json" }, + { "path": "./x-pack/plugins/ingest_pipelines/tsconfig.json" }, + { "path": "./x-pack/plugins/lens/tsconfig.json" }, + { "path": "./x-pack/plugins/license_management/tsconfig.json" }, + { "path": "./x-pack/plugins/licensing/tsconfig.json" }, + { "path": "./x-pack/plugins/maps_file_upload/tsconfig.json" }, + { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, + { "path": "./x-pack/plugins/maps/tsconfig.json" }, + { "path": "./x-pack/plugins/ml/tsconfig.json" }, + { "path": "./x-pack/plugins/observability/tsconfig.json" }, + { "path": "./x-pack/plugins/painless_lab/tsconfig.json" }, + { "path": "./x-pack/plugins/reporting/tsconfig.json" }, + { "path": "./x-pack/plugins/saved_objects_tagging/tsconfig.json" }, + { "path": "./x-pack/plugins/searchprofiler/tsconfig.json" }, + { "path": "./x-pack/plugins/security/tsconfig.json" }, + { "path": "./x-pack/plugins/snapshot_restore/tsconfig.json" }, + { "path": "./x-pack/plugins/spaces/tsconfig.json" }, + { "path": "./x-pack/plugins/stack_alerts/tsconfig.json" }, + { "path": "./x-pack/plugins/task_manager/tsconfig.json" }, + { "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "./x-pack/plugins/transform/tsconfig.json" }, + { "path": "./x-pack/plugins/translations/tsconfig.json" }, + { "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" }, + { "path": "./x-pack/plugins/ui_actions_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/upgrade_assistant/tsconfig.json" }, + { "path": "./x-pack/plugins/runtime_fields/tsconfig.json" }, + { "path": "./x-pack/plugins/index_management/tsconfig.json" }, + { "path": "./x-pack/plugins/watcher/tsconfig.json" }, + { "path": "./x-pack/plugins/rollup/tsconfig.json"}, + { "path": "./x-pack/plugins/remote_clusters/tsconfig.json"}, + { "path": "./x-pack/plugins/cross_cluster_replication/tsconfig.json"}, + { "path": "./x-pack/plugins/index_lifecycle_management/tsconfig.json"}, + { "path": "./x-pack/plugins/uptime/tsconfig.json" }, ] } From 0d94968df1b7e9d3fc3d24abcc7797ba70983090 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Tue, 9 Feb 2021 17:40:18 -0600 Subject: [PATCH 085/100] [Metrics UI] Add Metrics Anomaly Alert Type (#89244) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../infra/common/alerting/metrics/types.ts | 43 ++- .../infra/common/infra_ml/anomaly_results.ts | 56 +-- .../common/components/alert_preview.tsx | 3 +- .../common/components/get_alert_preview.ts | 4 +- .../components/metrics_alert_dropdown.tsx | 151 +++++++++ .../inventory/components/alert_dropdown.tsx | 59 ---- .../inventory/components/alert_flyout.tsx | 18 +- .../inventory/components/node_type.tsx | 2 +- .../components/alert_flyout.tsx | 53 +++ .../components/expression.test.tsx | 74 ++++ .../metric_anomaly/components/expression.tsx | 320 ++++++++++++++++++ .../components/influencer_filter.tsx | 193 +++++++++++ .../metric_anomaly/components/node_type.tsx | 117 +++++++ .../components/severity_threshold.tsx | 140 ++++++++ .../metric_anomaly/components/validation.tsx | 35 ++ .../public/alerting/metric_anomaly/index.ts | 46 +++ .../components/alert_dropdown.tsx | 57 ---- .../components/alert_flyout.tsx | 11 +- .../logging/log_analysis_setup/index.ts | 1 - .../subscription_splash_content.tsx | 176 ---------- .../source_configuration_settings.tsx | 4 +- .../subscription_splash_content.tsx | 110 +++--- .../containers/ml/infra_ml_capabilities.tsx | 4 +- .../containers/with_kuery_autocompletion.tsx | 11 +- .../log_entry_categories/page_content.tsx | 2 +- .../logs/log_entry_rate/page_content.tsx | 2 +- .../infra/public/pages/metrics/index.tsx | 8 +- ...lyout.tsx => anomaly_detection_flyout.tsx} | 0 .../ml/anomaly_detection/flyout_home.tsx | 6 +- .../metrics_explorer/components/kuery_bar.tsx | 21 +- x-pack/plugins/infra/public/plugin.ts | 2 + x-pack/plugins/infra/public/types.ts | 3 +- .../metric_anomaly/evaluate_condition.ts | 51 +++ .../metric_anomaly/metric_anomaly_executor.ts | 142 ++++++++ .../preview_metric_anomaly_alert.ts | 120 +++++++ .../register_metric_anomaly_alert_type.ts | 110 ++++++ .../lib/alerting/register_alert_types.ts | 10 +- .../infra/server/lib/infra_ml/common.ts | 17 + .../infra/server/lib/infra_ml/index.ts | 1 + .../lib/infra_ml/metrics_hosts_anomalies.ts | 39 +-- .../lib/infra_ml/metrics_k8s_anomalies.ts | 39 +-- .../server/lib/infra_ml/queries/common.ts | 32 ++ .../queries/metrics_hosts_anomalies.ts | 12 +- .../infra_ml/queries/metrics_k8s_anomalies.ts | 12 +- x-pack/plugins/infra/server/plugin.ts | 2 +- .../infra/server/routes/alerting/preview.ts | 51 ++- .../results/metrics_hosts_anomalies.ts | 2 +- .../infra_ml/results/metrics_k8s_anomalies.ts | 2 +- .../translations/translations/ja-JP.json | 11 - .../translations/translations/zh-CN.json | 11 - 50 files changed, 1917 insertions(+), 479 deletions(-) create mode 100644 x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts delete mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx delete mode 100644 x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx rename x-pack/plugins/infra/public/{pages/metrics/inventory_view/components/ml/anomaly_detection => components}/subscription_splash_content.tsx (58%) rename x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/{anomoly_detection_flyout.tsx => anomaly_detection_flyout.tsx} (100%) create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index a89f82e931fd4..7a4edb8f49189 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -4,14 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import * as rt from 'io-ts'; +import { ANOMALY_THRESHOLD } from '../../infra_ml'; import { ItemTypeRT } from '../../inventory_models/types'; // TODO: Have threshold and inventory alerts import these types from this file instead of from their // local directories export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; +export const METRIC_ANOMALY_ALERT_TYPE_ID = 'metrics.alert.anomaly'; export enum Comparator { GT = '>', @@ -34,6 +35,26 @@ export enum Aggregators { P99 = 'p99', } +const metricAnomalyNodeTypeRT = rt.union([rt.literal('hosts'), rt.literal('k8s')]); +const metricAnomalyMetricRT = rt.union([ + rt.literal('memory_usage'), + rt.literal('network_in'), + rt.literal('network_out'), +]); +const metricAnomalyInfluencerFilterRT = rt.type({ + fieldName: rt.string, + fieldValue: rt.string, +}); + +export interface MetricAnomalyParams { + nodeType: rt.TypeOf; + metric: rt.TypeOf; + alertInterval?: string; + sourceId?: string; + threshold: Exclude; + influencerFilter: rt.TypeOf | undefined; +} + // Alert Preview API const baseAlertRequestParamsRT = rt.intersection([ rt.partial({ @@ -51,7 +72,6 @@ const baseAlertRequestParamsRT = rt.intersection([ rt.literal('M'), rt.literal('y'), ]), - criteria: rt.array(rt.any), alertInterval: rt.string, alertThrottle: rt.string, alertOnNoData: rt.boolean, @@ -65,6 +85,7 @@ const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([ }), rt.type({ alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID), + criteria: rt.array(rt.any), }), ]); export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf< @@ -76,15 +97,33 @@ const inventoryAlertPreviewRequestParamsRT = rt.intersection([ rt.type({ nodeType: ItemTypeRT, alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID), + criteria: rt.array(rt.any), }), ]); export type InventoryAlertPreviewRequestParams = rt.TypeOf< typeof inventoryAlertPreviewRequestParamsRT >; +const metricAnomalyAlertPreviewRequestParamsRT = rt.intersection([ + baseAlertRequestParamsRT, + rt.type({ + nodeType: metricAnomalyNodeTypeRT, + metric: metricAnomalyMetricRT, + threshold: rt.number, + alertType: rt.literal(METRIC_ANOMALY_ALERT_TYPE_ID), + }), + rt.partial({ + influencerFilter: metricAnomalyInfluencerFilterRT, + }), +]); +export type MetricAnomalyAlertPreviewRequestParams = rt.TypeOf< + typeof metricAnomalyAlertPreviewRequestParamsRT +>; + export const alertPreviewRequestParamsRT = rt.union([ metricThresholdAlertPreviewRequestParamsRT, inventoryAlertPreviewRequestParamsRT, + metricAnomalyAlertPreviewRequestParamsRT, ]); export type AlertPreviewRequestParams = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts index 589e57a1388b5..81e46d85ba220 100644 --- a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts +++ b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts @@ -5,36 +5,44 @@ * 2.0. */ -export const ML_SEVERITY_SCORES = { - warning: 3, - minor: 25, - major: 50, - critical: 75, -}; +export enum ANOMALY_SEVERITY { + CRITICAL = 'critical', + MAJOR = 'major', + MINOR = 'minor', + WARNING = 'warning', + LOW = 'low', + UNKNOWN = 'unknown', +} -export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES; +export enum ANOMALY_THRESHOLD { + CRITICAL = 75, + MAJOR = 50, + MINOR = 25, + WARNING = 3, + LOW = 0, +} -export const ML_SEVERITY_COLORS = { - critical: 'rgb(228, 72, 72)', - major: 'rgb(229, 113, 0)', - minor: 'rgb(255, 221, 0)', - warning: 'rgb(125, 180, 226)', +export const SEVERITY_COLORS = { + CRITICAL: '#fe5050', + MAJOR: '#fba740', + MINOR: '#fdec25', + WARNING: '#8bc8fb', + LOW: '#d2e9f7', + BLANK: '#ffffff', }; -export const getSeverityCategoryForScore = ( - score: number -): MLSeverityScoreCategories | undefined => { - if (score >= ML_SEVERITY_SCORES.critical) { - return 'critical'; - } else if (score >= ML_SEVERITY_SCORES.major) { - return 'major'; - } else if (score >= ML_SEVERITY_SCORES.minor) { - return 'minor'; - } else if (score >= ML_SEVERITY_SCORES.warning) { - return 'warning'; +export const getSeverityCategoryForScore = (score: number): ANOMALY_SEVERITY | undefined => { + if (score >= ANOMALY_THRESHOLD.CRITICAL) { + return ANOMALY_SEVERITY.CRITICAL; + } else if (score >= ANOMALY_THRESHOLD.MAJOR) { + return ANOMALY_SEVERITY.MAJOR; + } else if (score >= ANOMALY_THRESHOLD.MINOR) { + return ANOMALY_SEVERITY.MINOR; + } else if (score >= ANOMALY_THRESHOLD.WARNING) { + return ANOMALY_SEVERITY.WARNING; } else { // Category is too low to include - return undefined; + return ANOMALY_SEVERITY.LOW; } }; diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index fac87e20dfe7d..0135e185e846a 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -37,7 +37,7 @@ interface Props { alertInterval: string; alertThrottle: string; alertType: PreviewableAlertTypes; - alertParams: { criteria: any[]; sourceId: string } & Record; + alertParams: { criteria?: any[]; sourceId: string } & Record; validate: (params: any) => ValidationResult; showNoDataResults?: boolean; groupByDisplayName?: string; @@ -109,6 +109,7 @@ export const AlertPreview: React.FC = (props) => { }, [previewLookbackInterval, alertInterval]); const isPreviewDisabled = useMemo(() => { + if (!alertParams.criteria) return false; const validationResult = validate({ criteria: alertParams.criteria } as any); const hasValidationErrors = Object.values(validationResult.errors).some((result) => Object.values(result).some((arr) => Array.isArray(arr) && arr.length) diff --git a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts index a1cee1361a18f..2bb98e83cbe70 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts +++ b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts @@ -10,13 +10,15 @@ import { INFRA_ALERT_PREVIEW_PATH, METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + METRIC_ANOMALY_ALERT_TYPE_ID, AlertPreviewRequestParams, AlertPreviewSuccessResponsePayload, } from '../../../../common/alerting/metrics'; export type PreviewableAlertTypes = | typeof METRIC_THRESHOLD_ALERT_TYPE_ID - | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID; + | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID + | typeof METRIC_ANOMALY_ALERT_TYPE_ID; export async function getAlertPreview({ fetch, diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx new file mode 100644 index 0000000000000..f1236c4fc2c2b --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState, useCallback, useMemo } from 'react'; +import { + EuiPopover, + EuiButtonEmpty, + EuiContextMenu, + EuiContextMenuPanelDescriptor, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities'; +import { PrefilledInventoryAlertFlyout } from '../../inventory/components/alert_flyout'; +import { PrefilledThresholdAlertFlyout } from '../../metric_threshold/components/alert_flyout'; +import { PrefilledAnomalyAlertFlyout } from '../../metric_anomaly/components/alert_flyout'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +type VisibleFlyoutType = 'inventory' | 'threshold' | 'anomaly' | null; + +export const MetricsAlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [visibleFlyoutType, setVisibleFlyoutType] = useState(null); + const { hasInfraMLCapabilities } = useInfraMLCapabilities(); + + const closeFlyout = useCallback(() => setVisibleFlyoutType(null), [setVisibleFlyoutType]); + + const manageAlertsLinkProps = useLinkProps({ + app: 'management', + pathname: '/insightsAndAlerting/triggersActions/alerts', + }); + + const panels: EuiContextMenuPanelDescriptor[] = useMemo( + () => [ + { + id: 0, + title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', { + defaultMessage: 'Alerts', + }), + items: [ + { + name: i18n.translate('xpack.infra.alerting.infrastructureDropdownMenu', { + defaultMessage: 'Infrastructure', + }), + panel: 1, + }, + { + name: i18n.translate('xpack.infra.alerting.metricsDropdownMenu', { + defaultMessage: 'Metrics', + }), + panel: 2, + }, + { + name: i18n.translate('xpack.infra.alerting.manageAlerts', { + defaultMessage: 'Manage alerts', + }), + icon: 'tableOfContents', + onClick: manageAlertsLinkProps.onClick, + }, + ], + }, + { + id: 1, + title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', { + defaultMessage: 'Infrastructure alerts', + }), + items: [ + { + name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', { + defaultMessage: 'Create inventory alert', + }), + onClick: () => setVisibleFlyoutType('inventory'), + }, + ].concat( + hasInfraMLCapabilities + ? { + name: i18n.translate('xpack.infra.alerting.createAnomalyAlertButton', { + defaultMessage: 'Create anomaly alert', + }), + onClick: () => setVisibleFlyoutType('anomaly'), + } + : [] + ), + }, + { + id: 2, + title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', { + defaultMessage: 'Metrics alerts', + }), + items: [ + { + name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', { + defaultMessage: 'Create threshold alert', + }), + onClick: () => setVisibleFlyoutType('threshold'), + }, + ], + }, + ], + [manageAlertsLinkProps, setVisibleFlyoutType, hasInfraMLCapabilities] + ); + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + return ( + <> + + + + } + isOpen={popoverOpen} + closePopover={closePopover} + > + + + + + ); +}; + +interface AlertFlyoutProps { + visibleFlyoutType: VisibleFlyoutType; + onClose(): void; +} + +const AlertFlyout = ({ visibleFlyoutType, onClose }: AlertFlyoutProps) => { + switch (visibleFlyoutType) { + case 'inventory': + return ; + case 'threshold': + return ; + case 'anomaly': + return ; + default: + return null; + } +}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx deleted file mode 100644 index a7b6c9fb7104c..0000000000000 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useCallback } from 'react'; -import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; -import { AlertFlyout } from './alert_flyout'; -import { ManageAlertsContextMenuItem } from './manage_alerts_context_menu_item'; - -export const InventoryAlertDropdown = () => { - const [popoverOpen, setPopoverOpen] = useState(false); - const [flyoutVisible, setFlyoutVisible] = useState(false); - - const { inventoryPrefill } = useAlertPrefillContext(); - const { nodeType, metric, filterQuery } = inventoryPrefill; - - const closePopover = useCallback(() => { - setPopoverOpen(false); - }, [setPopoverOpen]); - - const openPopover = useCallback(() => { - setPopoverOpen(true); - }, [setPopoverOpen]); - - const menuItems = [ - setFlyoutVisible(true)}> - - , - , - ]; - - return ( - <> - - - - } - isOpen={popoverOpen} - closePopover={closePopover} - > - - - - - ); -}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index 815e1f2be33f2..33fe3c7af30c7 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -8,8 +8,7 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; import { InfraWaffleMapOptions } from '../../../lib/lib'; import { InventoryItemType } from '../../../../common/inventory_models/types'; import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; @@ -49,3 +48,18 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: return <>{visible && AddAlertFlyout}; }; + +export const PrefilledInventoryAlertFlyout = ({ onClose }: { onClose(): void }) => { + const { inventoryPrefill } = useAlertPrefillContext(); + const { nodeType, metric, filterQuery } = inventoryPrefill; + + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx index f02f98c49f01a..bd7812acac678 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx @@ -68,7 +68,7 @@ export const NodeTypeExpression = ({ setAggTypePopoverOpen(false)}> { + const { triggersActionsUI } = useContext(TriggerActionsContext); + + const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]); + const AddAlertFlyout = useMemo( + () => + triggersActionsUI && + triggersActionsUI.getAddAlertFlyout({ + consumer: 'infrastructure', + onClose: onCloseFlyout, + canChangeTrigger: false, + alertTypeId: METRIC_ANOMALY_ALERT_TYPE_ID, + metadata: { + metric, + nodeType, + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [triggersActionsUI, visible] + ); + + return <>{visible && AddAlertFlyout}; +}; + +export const PrefilledAnomalyAlertFlyout = ({ onClose }: { onClose(): void }) => { + const { inventoryPrefill } = useAlertPrefillContext(); + const { nodeType, metric } = inventoryPrefill; + + return ; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx new file mode 100644 index 0000000000000..ae2c6ed81badb --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` +import { coreMock as mockCoreMock } from 'src/core/public/mocks'; +import React from 'react'; +import { Expression, AlertContextMeta } from './expression'; +import { act } from 'react-dom/test-utils'; + +jest.mock('../../../containers/source/use_source_via_http', () => ({ + useSourceViaHttp: () => ({ + source: { id: 'default' }, + createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), + }), +})); + +jest.mock('../../../hooks/use_kibana', () => ({ + useKibanaContextForPlugin: () => ({ + services: mockCoreMock.createStart(), + }), +})); + +jest.mock('../../../containers/ml/infra_ml_capabilities', () => ({ + useInfraMLCapabilities: () => ({ + isLoading: false, + hasInfraMLCapabilities: true, + }), +})); + +describe('Expression', () => { + async function setup(currentOptions: AlertContextMeta) { + const alertParams = { + metric: undefined, + nodeType: undefined, + threshold: 50, + }; + const wrapper = mountWithIntl( + Reflect.set(alertParams, key, value)} + setAlertProperty={() => {}} + metadata={currentOptions} + /> + ); + + const update = async () => + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + await update(); + + return { wrapper, update, alertParams }; + } + + it('should prefill the alert using the context metadata', async () => { + const currentOptions = { + nodeType: 'pod', + metric: { type: 'tx' }, + }; + const { alertParams } = await setup(currentOptions as AlertContextMeta); + expect(alertParams.nodeType).toBe('k8s'); + expect(alertParams.metric).toBe('network_out'); + }); +}); diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx new file mode 100644 index 0000000000000..5938c7119616f --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx @@ -0,0 +1,320 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pick } from 'lodash'; +import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import { EuiFlexGroup, EuiSpacer, EuiText, EuiLoadingContent } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; +import { AlertPreview } from '../../common'; +import { + METRIC_ANOMALY_ALERT_TYPE_ID, + MetricAnomalyParams, +} from '../../../../common/alerting/metrics'; +import { euiStyled, EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import { + WhenExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { NodeTypeExpression } from './node_type'; +import { SeverityThresholdExpression } from './severity_threshold'; +import { InfraWaffleMapOptions } from '../../../lib/lib'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; + +import { validateMetricAnomaly } from './validation'; +import { InfluencerFilter } from './influencer_filter'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; + +export interface AlertContextMeta { + metric?: InfraWaffleMapOptions['metric']; + nodeType?: InventoryItemType; +} + +interface Props { + errors: IErrorObject[]; + alertParams: MetricAnomalyParams & { + sourceId: string; + }; + alertInterval: string; + alertThrottle: string; + setAlertParams(key: string, value: any): void; + setAlertProperty(key: string, value: any): void; + metadata: AlertContextMeta; +} + +export const defaultExpression = { + metric: 'memory_usage' as MetricAnomalyParams['metric'], + threshold: ANOMALY_THRESHOLD.MAJOR, + nodeType: 'hosts', + influencerFilter: undefined, +}; + +export const Expression: React.FC = (props) => { + const { hasInfraMLCapabilities, isLoading: isLoadingMLCapabilities } = useInfraMLCapabilities(); + const { http, notifications } = useKibanaContextForPlugin().services; + const { setAlertParams, alertParams, alertInterval, alertThrottle, metadata } = props; + const { source, createDerivedIndexPattern } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + fetch: http.fetch, + toastWarning: notifications.toasts.addWarning, + }); + + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + createDerivedIndexPattern, + ]); + + const [influencerFieldName, updateInfluencerFieldName] = useState( + alertParams.influencerFilter?.fieldName ?? 'host.name' + ); + + useEffect(() => { + setAlertParams('hasInfraMLCapabilities', hasInfraMLCapabilities); + }, [setAlertParams, hasInfraMLCapabilities]); + + useEffect(() => { + if (alertParams.influencerFilter) { + setAlertParams('influencerFilter', { + ...alertParams.influencerFilter, + fieldName: influencerFieldName, + }); + } + }, [influencerFieldName, alertParams, setAlertParams]); + const updateInfluencerFieldValue = useCallback( + (value: string) => { + if (value) { + setAlertParams('influencerFilter', { + ...alertParams.influencerFilter, + fieldValue: value, + }); + } else { + setAlertParams('influencerFilter', undefined); + } + }, + [setAlertParams, alertParams] + ); + + useEffect(() => { + setAlertParams('alertInterval', alertInterval); + }, [setAlertParams, alertInterval]); + + const updateNodeType = useCallback( + (nt: any) => { + setAlertParams('nodeType', nt); + }, + [setAlertParams] + ); + + const updateMetric = useCallback( + (metric: string) => { + setAlertParams('metric', metric); + }, + [setAlertParams] + ); + + const updateSeverityThreshold = useCallback( + (threshold: any) => { + setAlertParams('threshold', threshold); + }, + [setAlertParams] + ); + + const prefillNodeType = useCallback(() => { + const md = metadata; + if (md && md.nodeType) { + setAlertParams( + 'nodeType', + getMLNodeTypeFromInventoryNodeType(md.nodeType) ?? defaultExpression.nodeType + ); + } else { + setAlertParams('nodeType', defaultExpression.nodeType); + } + }, [metadata, setAlertParams]); + + const prefillMetric = useCallback(() => { + const md = metadata; + if (md && md.metric) { + setAlertParams( + 'metric', + getMLMetricFromInventoryMetric(md.metric.type) ?? defaultExpression.metric + ); + } else { + setAlertParams('metric', defaultExpression.metric); + } + }, [metadata, setAlertParams]); + + useEffect(() => { + if (!alertParams.nodeType) { + prefillNodeType(); + } + + if (!alertParams.threshold) { + setAlertParams('threshold', defaultExpression.threshold); + } + + if (!alertParams.metric) { + prefillMetric(); + } + + if (!alertParams.sourceId) { + setAlertParams('sourceId', source?.id || 'default'); + } + }, [metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + + if (isLoadingMLCapabilities) return ; + if (!hasInfraMLCapabilities) return ; + + return ( + // https://github.com/elastic/kibana/issues/89506 + + +

    - - + { jobSummaries: k8sJobSummaries, } = useMetricK8sModuleContext(); const { - hasInfraMLCapabilites, + hasInfraMLCapabilities, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities, } = useInfraMLCapabilitiesContext(); @@ -69,7 +69,7 @@ export const FlyoutHome = (props: Props) => { } }, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]); - if (!hasInfraMLCapabilites) { + if (!hasInfraMLCapabilities) { return ; } else if (!hasInfraMLReadCapabilities) { return ; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx index 44391568741f3..e22c6fa661181 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx @@ -10,7 +10,19 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../../../../components/autocomplete_field'; -import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { + esKuery, + IIndexPattern, + QuerySuggestion, +} from '../../../../../../../../src/plugins/data/public'; + +type LoadSuggestionsFn = ( + e: string, + p: number, + m?: number, + transform?: (s: QuerySuggestion[]) => QuerySuggestion[] +) => void; +export type CurryLoadSuggestionsType = (loadSuggestions: LoadSuggestionsFn) => LoadSuggestionsFn; interface Props { derivedIndexPattern: IIndexPattern; @@ -18,6 +30,7 @@ interface Props { onChange?: (query: string) => void; value?: string | null; placeholder?: string; + curryLoadSuggestions?: CurryLoadSuggestionsType; } function validateQuery(query: string) { @@ -35,6 +48,7 @@ export const MetricsExplorerKueryBar = ({ onChange, value, placeholder, + curryLoadSuggestions = defaultCurryLoadSuggestions, }: Props) => { const [draftQuery, setDraftQuery] = useState(value || ''); const [isValid, setValidation] = useState(true); @@ -73,7 +87,7 @@ export const MetricsExplorerKueryBar = ({ aria-label={placeholder} isLoadingSuggestions={isLoadingSuggestions} isValid={isValid} - loadSuggestions={loadSuggestions} + loadSuggestions={curryLoadSuggestions(loadSuggestions)} onChange={handleChange} onSubmit={onSubmit} placeholder={placeholder || defaultPlaceholder} @@ -84,3 +98,6 @@ export const MetricsExplorerKueryBar = ({ ); }; + +const defaultCurryLoadSuggestions: CurryLoadSuggestionsType = (loadSuggestions) => (...args) => + loadSuggestions(...args); diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 8e7d165f8a535..d4bb83e8668ba 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -10,6 +10,7 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { createMetricThresholdAlertType } from './alerting/metric_threshold'; import { createInventoryMetricAlertType } from './alerting/inventory'; +import { createMetricAnomalyAlertType } from './alerting/metric_anomaly'; import { getAlertType as getLogsAlertType } from './alerting/log_threshold'; import { registerFeatures } from './register_feature'; import { @@ -35,6 +36,7 @@ export class Plugin implements InfraClientPluginClass { pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType()); pluginsSetup.triggersActionsUi.alertTypeRegistry.register(getLogsAlertType()); pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType()); + pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricAnomalyAlertType()); if (pluginsSetup.observability) { pluginsSetup.observability.dashboard.register({ diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index b18b6e8a6eba6..4d70676d25e40 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -23,7 +23,7 @@ import type { ObservabilityPluginStart, } from '../../observability/public'; import type { SpacesPluginStart } from '../../spaces/public'; -import { MlPluginStart } from '../../ml/public'; +import { MlPluginStart, MlPluginSetup } from '../../ml/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; // Our own setup and start contract values @@ -36,6 +36,7 @@ export interface InfraClientSetupDeps { observability: ObservabilityPluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionSetup; + ml: MlPluginSetup; embeddable: EmbeddableSetup; } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts new file mode 100644 index 0000000000000..b7ef8ec7d2312 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; +import { getMetricsHostsAnomalies, getMetricK8sAnomalies } from '../../infra_ml'; +import { MlSystem, MlAnomalyDetectors } from '../../../types'; + +type ConditionParams = Omit & { + spaceId: string; + startTime: number; + endTime: number; + mlSystem: MlSystem; + mlAnomalyDetectors: MlAnomalyDetectors; +}; + +export const evaluateCondition = async ({ + nodeType, + spaceId, + sourceId, + mlSystem, + mlAnomalyDetectors, + startTime, + endTime, + metric, + threshold, + influencerFilter, +}: ConditionParams) => { + const getAnomalies = nodeType === 'k8s' ? getMetricK8sAnomalies : getMetricsHostsAnomalies; + + const result = await getAnomalies( + { + spaceId, + mlSystem, + mlAnomalyDetectors, + }, + sourceId ?? 'default', + threshold, + startTime, + endTime, + metric, + { field: 'anomalyScore', direction: 'desc' }, + { pageSize: 100 }, + influencerFilter + ); + + return result; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts new file mode 100644 index 0000000000000..ec95aac7268ad --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { first } from 'lodash'; +import moment from 'moment'; +import { stateToAlertMessage } from '../common/messages'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; +import { MappedAnomalyHit } from '../../infra_ml'; +import { AlertStates } from '../common/types'; +import { + ActionGroup, + AlertInstanceContext, + AlertInstanceState, +} from '../../../../../alerts/common'; +import { AlertExecutorOptions } from '../../../../../alerts/server'; +import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { MetricAnomalyAllowedActionGroups } from './register_metric_anomaly_alert_type'; +import { MlPluginSetup } from '../../../../../ml/server'; +import { KibanaRequest } from '../../../../../../../src/core/server'; +import { InfraBackendLibs } from '../../infra_types'; +import { evaluateCondition } from './evaluate_condition'; + +export const createMetricAnomalyExecutor = (libs: InfraBackendLibs, ml?: MlPluginSetup) => async ({ + services, + params, + startedAt, +}: AlertExecutorOptions< + /** + * TODO: Remove this use of `any` by utilizing a proper type + */ + Record, + Record, + AlertInstanceState, + AlertInstanceContext, + MetricAnomalyAllowedActionGroups +>) => { + if (!ml) { + return; + } + const request = {} as KibanaRequest; + const mlSystem = ml.mlSystemProvider(request, services.savedObjectsClient); + const mlAnomalyDetectors = ml.anomalyDetectorsProvider(request, services.savedObjectsClient); + + const { + metric, + alertInterval, + influencerFilter, + sourceId, + nodeType, + threshold, + } = params as MetricAnomalyParams; + + const alertInstance = services.alertInstanceFactory(`${nodeType}-${metric}`); + + const bucketInterval = getIntervalInSeconds('15m') * 1000; + const alertIntervalInMs = getIntervalInSeconds(alertInterval ?? '1m') * 1000; + + const endTime = startedAt.getTime(); + // Anomalies are bucketed at :00, :15, :30, :45 minutes every hour + const previousBucketStartTime = endTime - (endTime % bucketInterval); + + // If the alert interval is less than 15m, make sure that it actually queries an anomaly bucket + const startTime = Math.min(endTime - alertIntervalInMs, previousBucketStartTime); + + const { data } = await evaluateCondition({ + sourceId: sourceId ?? 'default', + spaceId: 'default', + mlSystem, + mlAnomalyDetectors, + startTime, + endTime, + metric, + threshold, + nodeType, + influencerFilter, + }); + + const shouldAlertFire = data.length > 0; + + if (shouldAlertFire) { + const { startTime: anomalyStartTime, anomalyScore, actual, typical, influencers } = first( + data as MappedAnomalyHit[] + )!; + + alertInstance.scheduleActions(FIRED_ACTIONS_ID, { + alertState: stateToAlertMessage[AlertStates.ALERT], + timestamp: moment(anomalyStartTime).toISOString(), + anomalyScore, + actual, + typical, + metric: metricNameMap[metric], + summary: generateSummaryMessage(actual, typical), + influencers: influencers.join(', '), + }); + } +}; + +export const FIRED_ACTIONS_ID = 'metrics.anomaly.fired'; +export const FIRED_ACTIONS: ActionGroup = { + id: FIRED_ACTIONS_ID, + name: i18n.translate('xpack.infra.metrics.alerting.anomaly.fired', { + defaultMessage: 'Fired', + }), +}; + +const generateSummaryMessage = (actual: number, typical: number) => { + const differential = (Math.max(actual, typical) / Math.min(actual, typical)) + .toFixed(1) + .replace('.0', ''); + if (actual > typical) { + return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryHigher', { + defaultMessage: '{differential}x higher', + values: { + differential, + }, + }); + } else { + return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryLower', { + defaultMessage: '{differential}x lower', + values: { + differential, + }, + }); + } +}; + +const metricNameMap = { + memory_usage: i18n.translate('xpack.infra.metrics.alerting.anomaly.memoryUsage', { + defaultMessage: 'Memory usage', + }), + network_in: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkIn', { + defaultMessage: 'Network in', + }), + network_out: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkOut', { + defaultMessage: 'Network out', + }), +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts new file mode 100644 index 0000000000000..98992701e3bb4 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Unit } from '@elastic/datemath'; +import { countBy } from 'lodash'; +import { MappedAnomalyHit } from '../../infra_ml'; +import { MlSystem, MlAnomalyDetectors } from '../../../types'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; +import { + TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, + isTooManyBucketsPreviewException, +} from '../../../../common/alerting/metrics'; +import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { evaluateCondition } from './evaluate_condition'; + +interface PreviewMetricAnomalyAlertParams { + mlSystem: MlSystem; + mlAnomalyDetectors: MlAnomalyDetectors; + spaceId: string; + params: MetricAnomalyParams; + sourceId: string; + lookback: Unit; + alertInterval: string; + alertThrottle: string; + alertOnNoData: boolean; +} + +export const previewMetricAnomalyAlert = async ({ + mlSystem, + mlAnomalyDetectors, + spaceId, + params, + sourceId, + lookback, + alertInterval, + alertThrottle, +}: PreviewMetricAnomalyAlertParams) => { + const { metric, threshold, influencerFilter, nodeType } = params as MetricAnomalyParams; + + const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); + const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle); + const executionsPerThrottle = Math.floor(throttleIntervalInSeconds / alertIntervalInSeconds); + + const lookbackInterval = `1${lookback}`; + const lookbackIntervalInSeconds = getIntervalInSeconds(lookbackInterval); + const endTime = Date.now(); + const startTime = endTime - lookbackIntervalInSeconds * 1000; + + const numberOfExecutions = Math.floor(lookbackIntervalInSeconds / alertIntervalInSeconds); + const bucketIntervalInSeconds = getIntervalInSeconds('15m'); + const bucketsPerExecution = Math.max( + 1, + Math.floor(alertIntervalInSeconds / bucketIntervalInSeconds) + ); + + try { + let anomalies: MappedAnomalyHit[] = []; + const { data } = await evaluateCondition({ + nodeType, + spaceId, + sourceId, + mlSystem, + mlAnomalyDetectors, + startTime, + endTime, + metric, + threshold, + influencerFilter, + }); + anomalies = [...anomalies, ...data]; + + const anomaliesByTime = countBy(anomalies, ({ startTime: anomStartTime }) => anomStartTime); + + let numberOfTimesFired = 0; + let numberOfNotifications = 0; + let throttleTracker = 0; + const notifyWithThrottle = () => { + if (throttleTracker === 0) numberOfNotifications++; + throttleTracker++; + }; + // Mock each alert evaluation + for (let i = 0; i < numberOfExecutions; i++) { + const executionTime = startTime + alertIntervalInSeconds * 1000 * i; + // Get an array of bucket times this mock alert evaluation will be looking at + // Anomalies are bucketed at :00, :15, :30, :45 minutes every hour, + // so this is an array of how many of those times occurred between this evaluation + // and the previous one + const bucketsLookedAt = Array.from(Array(bucketsPerExecution), (_, idx) => { + const previousBucketStartTime = + executionTime - + (executionTime % (bucketIntervalInSeconds * 1000)) - + idx * bucketIntervalInSeconds * 1000; + return previousBucketStartTime; + }); + const anomaliesDetectedInBuckets = bucketsLookedAt.some((bucketTime) => + Reflect.has(anomaliesByTime, bucketTime) + ); + + if (anomaliesDetectedInBuckets) { + numberOfTimesFired++; + notifyWithThrottle(); + } else if (throttleTracker > 0) { + throttleTracker++; + } + if (throttleTracker === executionsPerThrottle) { + throttleTracker = 0; + } + } + + return { fired: numberOfTimesFired, notifications: numberOfNotifications }; + } catch (e) { + if (!isTooManyBucketsPreviewException(e)) throw e; + const { maxBuckets } = e; + throw new Error(`${TOO_MANY_BUCKETS_PREVIEW_EXCEPTION}:${maxBuckets}`); + } +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts new file mode 100644 index 0000000000000..8ac62c125515a --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { MlPluginSetup } from '../../../../../ml/server'; +import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../../alerts/server'; +import { + createMetricAnomalyExecutor, + FIRED_ACTIONS, + FIRED_ACTIONS_ID, +} from './metric_anomaly_executor'; +import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; +import { InfraBackendLibs } from '../../infra_types'; +import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils'; +import { alertStateActionVariableDescription } from '../common/messages'; +import { RecoveredActionGroupId } from '../../../../../alerts/common'; + +export type MetricAnomalyAllowedActionGroups = typeof FIRED_ACTIONS_ID; + +export const registerMetricAnomalyAlertType = ( + libs: InfraBackendLibs, + ml?: MlPluginSetup +): AlertType< + /** + * TODO: Remove this use of `any` by utilizing a proper type + */ + Record, + Record, + AlertInstanceState, + AlertInstanceContext, + MetricAnomalyAllowedActionGroups, + RecoveredActionGroupId +> => ({ + id: METRIC_ANOMALY_ALERT_TYPE_ID, + name: i18n.translate('xpack.infra.metrics.anomaly.alertName', { + defaultMessage: 'Infrastructure anomaly', + }), + validate: { + params: schema.object( + { + nodeType: oneOfLiterals(['hosts', 'k8s']), + alertInterval: schema.string(), + metric: oneOfLiterals(['memory_usage', 'network_in', 'network_out']), + threshold: schema.number(), + filterQuery: schema.maybe( + schema.string({ validate: validateIsStringElasticsearchJSONFilter }) + ), + sourceId: schema.string(), + }, + { unknowns: 'allow' } + ), + }, + defaultActionGroupId: FIRED_ACTIONS_ID, + actionGroups: [FIRED_ACTIONS], + producer: 'infrastructure', + minimumLicenseRequired: 'basic', + executor: createMetricAnomalyExecutor(libs, ml), + actionVariables: { + context: [ + { name: 'alertState', description: alertStateActionVariableDescription }, + { + name: 'metric', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyMetricDescription', { + defaultMessage: 'The metric name in the specified condition.', + }), + }, + { + name: 'timestamp', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyTimestampDescription', { + defaultMessage: 'A timestamp of when the anomaly was detected.', + }), + }, + { + name: 'anomalyScore', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyScoreDescription', { + defaultMessage: 'The exact severity score of the detected anomaly.', + }), + }, + { + name: 'actual', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyActualDescription', { + defaultMessage: 'The actual value of the monitored metric at the time of the anomaly.', + }), + }, + { + name: 'typical', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyTypicalDescription', { + defaultMessage: 'The typical value of the monitored metric at the time of the anomaly.', + }), + }, + { + name: 'summary', + description: i18n.translate('xpack.infra.metrics.alerting.anomalySummaryDescription', { + defaultMessage: 'A description of the anomaly, e.g. "2x higher."', + }), + }, + { + name: 'influencers', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyInfluencersDescription', { + defaultMessage: 'A list of node names that influenced the anomaly.', + }), + }, + ], + }, +}); diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index 0b4df6805759e..11fbe269b854d 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -8,13 +8,21 @@ import { PluginSetupContract } from '../../../../alerts/server'; import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type'; +import { registerMetricAnomalyAlertType } from './metric_anomaly/register_metric_anomaly_alert_type'; + import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type'; import { InfraBackendLibs } from '../infra_types'; +import { MlPluginSetup } from '../../../../ml/server'; -const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => { +const registerAlertTypes = ( + alertingPlugin: PluginSetupContract, + libs: InfraBackendLibs, + ml?: MlPluginSetup +) => { if (alertingPlugin) { alertingPlugin.registerType(registerMetricThresholdAlertType(libs)); alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs)); + alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml)); const registerFns = [registerLogThresholdAlertType]; registerFns.forEach((fn) => { diff --git a/x-pack/plugins/infra/server/lib/infra_ml/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/common.ts index 0182cb0e4099a..686f27d714cc1 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/common.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/common.ts @@ -17,6 +17,23 @@ import { import { decodeOrThrow } from '../../../common/runtime_types'; import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing'; +export interface MappedAnomalyHit { + id: string; + anomalyScore: number; + typical: number; + actual: number; + jobId: string; + startTime: number; + duration: number; + influencers: string[]; + categoryId?: string; +} + +export interface InfluencerFilter { + fieldName: string; + fieldValue: string; +} + export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) { const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES'); const { diff --git a/x-pack/plugins/infra/server/lib/infra_ml/index.ts b/x-pack/plugins/infra/server/lib/infra_ml/index.ts index d346b71d76aa8..82093b1a359d0 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/index.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/index.ts @@ -8,3 +8,4 @@ export * from './errors'; export * from './metrics_hosts_anomalies'; export * from './metrics_k8s_anomalies'; +export { MappedAnomalyHit } from './common'; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts index 7873fd8e43a7b..f6e11f5294191 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts @@ -5,11 +5,10 @@ * 2.0. */ -import type { InfraPluginRequestHandlerContext } from '../../types'; import { InfraRequestHandlerContext } from '../../types'; import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob } from './common'; -import { getJobId, metricsHostsJobTypes } from '../../../common/infra_ml'; +import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common'; +import { getJobId, metricsHostsJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml'; import { Sort, Pagination } from '../../../common/http_api/infra_ml'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; @@ -19,18 +18,6 @@ import { createMetricsHostsAnomaliesQuery, } from './queries/metrics_hosts_anomalies'; -interface MappedAnomalyHit { - id: string; - anomalyScore: number; - typical: number; - actual: number; - jobId: string; - startTime: number; - duration: number; - influencers: string[]; - categoryId?: string; -} - async function getCompatibleAnomaliesJobIds( spaceId: string, sourceId: string, @@ -74,14 +61,15 @@ async function getCompatibleAnomaliesJobIds( } export async function getMetricsHostsAnomalies( - context: InfraPluginRequestHandlerContext & { infra: Required }, + context: Required, sourceId: string, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter ) { const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies'); @@ -89,10 +77,10 @@ export async function getMetricsHostsAnomalies( jobIds, timing: { spans: jobSpans }, } = await getCompatibleAnomaliesJobIds( - context.infra.spaceId, + context.spaceId, sourceId, metric, - context.infra.mlAnomalyDetectors + context.mlAnomalyDetectors ); if (jobIds.length === 0) { @@ -108,13 +96,14 @@ export async function getMetricsHostsAnomalies( hasMoreEntries, timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricsHostsAnomalies( - context.infra.mlSystem, + context.mlSystem, anomalyThreshold, jobIds, startTime, endTime, sort, - pagination + pagination, + influencerFilter ); const data = anomalies.map((anomaly) => { @@ -164,12 +153,13 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricsHostsAnomalies( mlSystem: MlSystem, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, jobIds: string[], startTime: number, endTime: number, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter ) { // We'll request 1 extra entry on top of our pageSize to determine if there are // more entries to be fetched. This avoids scenarios where the client side can't @@ -188,6 +178,7 @@ async function fetchMetricsHostsAnomalies( endTime, sort, pagination: expandedPagination, + influencerFilter, }), jobIds ) diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts index 0c87b2f0f8b53..34039e9107f00 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts @@ -5,11 +5,10 @@ * 2.0. */ -import type { InfraPluginRequestHandlerContext } from '../../types'; import { InfraRequestHandlerContext } from '../../types'; import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob } from './common'; -import { getJobId, metricsK8SJobTypes } from '../../../common/infra_ml'; +import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common'; +import { getJobId, metricsK8SJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml'; import { Sort, Pagination } from '../../../common/http_api/infra_ml'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; @@ -19,18 +18,6 @@ import { createMetricsK8sAnomaliesQuery, } from './queries/metrics_k8s_anomalies'; -interface MappedAnomalyHit { - id: string; - anomalyScore: number; - typical: number; - actual: number; - jobId: string; - startTime: number; - influencers: string[]; - duration: number; - categoryId?: string; -} - async function getCompatibleAnomaliesJobIds( spaceId: string, sourceId: string, @@ -74,14 +61,15 @@ async function getCompatibleAnomaliesJobIds( } export async function getMetricK8sAnomalies( - context: InfraPluginRequestHandlerContext & { infra: Required }, + context: Required, sourceId: string, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter ) { const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies'); @@ -89,10 +77,10 @@ export async function getMetricK8sAnomalies( jobIds, timing: { spans: jobSpans }, } = await getCompatibleAnomaliesJobIds( - context.infra.spaceId, + context.spaceId, sourceId, metric, - context.infra.mlAnomalyDetectors + context.mlAnomalyDetectors ); if (jobIds.length === 0) { @@ -107,13 +95,14 @@ export async function getMetricK8sAnomalies( hasMoreEntries, timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricK8sAnomalies( - context.infra.mlSystem, + context.mlSystem, anomalyThreshold, jobIds, startTime, endTime, sort, - pagination + pagination, + influencerFilter ); const data = anomalies.map((anomaly) => { @@ -160,12 +149,13 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricK8sAnomalies( mlSystem: MlSystem, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, jobIds: string[], startTime: number, endTime: number, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter | undefined ) { // We'll request 1 extra entry on top of our pageSize to determine if there are // more entries to be fetched. This avoids scenarios where the client side can't @@ -184,6 +174,7 @@ async function fetchMetricK8sAnomalies( endTime, sort, pagination: expandedPagination, + influencerFilter, }), jobIds ) diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts index b3676fc54aeaa..6f996a672a44a 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts @@ -77,3 +77,35 @@ export const createDatasetsFilters = (datasets?: string[]) => }, ] : []; + +export const createInfluencerFilter = ({ + fieldName, + fieldValue, +}: { + fieldName: string; + fieldValue: string; +}) => [ + { + nested: { + path: 'influencers', + query: { + bool: { + must: [ + { + match: { + 'influencers.influencer_field_name': fieldName, + }, + }, + { + query_string: { + fields: ['influencers.influencer_field_values'], + query: fieldValue, + minimum_should_match: 1, + }, + }, + ], + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts index 45587cd258e5d..7808851508a7c 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { createJobIdsFilters, @@ -13,7 +14,9 @@ import { createResultTypeFilters, defaultRequestParameters, createAnomalyScoreFilter, + createInfluencerFilter, } from './common'; +import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; // TODO: Reassess validity of this against ML docs @@ -32,13 +35,15 @@ export const createMetricsHostsAnomaliesQuery = ({ endTime, sort, pagination, + influencerFilter, }: { jobIds: string[]; - anomalyThreshold: number; + anomalyThreshold: ANOMALY_THRESHOLD; startTime: number; endTime: number; sort: Sort; pagination: Pagination; + influencerFilter?: InfluencerFilter; }) => { const { field } = sort; const { pageSize } = pagination; @@ -50,6 +55,10 @@ export const createMetricsHostsAnomaliesQuery = ({ ...createResultTypeFilters(['record']), ]; + const influencerQuery = influencerFilter + ? { must: createInfluencerFilter(influencerFilter) } + : {}; + const sourceFields = [ 'job_id', 'record_score', @@ -77,6 +86,7 @@ export const createMetricsHostsAnomaliesQuery = ({ query: { bool: { filter: filters, + ...influencerQuery, }, }, search_after: queryCursor, diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts index 56a4b99e7236c..54eea067177ed 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { createJobIdsFilters, @@ -13,7 +14,9 @@ import { createResultTypeFilters, defaultRequestParameters, createAnomalyScoreFilter, + createInfluencerFilter, } from './common'; +import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; // TODO: Reassess validity of this against ML docs @@ -32,13 +35,15 @@ export const createMetricsK8sAnomaliesQuery = ({ endTime, sort, pagination, + influencerFilter, }: { jobIds: string[]; - anomalyThreshold: number; + anomalyThreshold: ANOMALY_THRESHOLD; startTime: number; endTime: number; sort: Sort; pagination: Pagination; + influencerFilter?: InfluencerFilter; }) => { const { field } = sort; const { pageSize } = pagination; @@ -50,6 +55,10 @@ export const createMetricsK8sAnomaliesQuery = ({ ...createResultTypeFilters(['record']), ]; + const influencerQuery = influencerFilter + ? { must: createInfluencerFilter(influencerFilter) } + : {}; + const sourceFields = [ 'job_id', 'record_score', @@ -76,6 +85,7 @@ export const createMetricsK8sAnomaliesQuery = ({ query: { bool: { filter: filters, + ...influencerQuery, }, }, search_after: queryCursor, diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 99555fa56acd5..0ac49e05b36b9 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -137,7 +137,7 @@ export class InfraServerPlugin implements Plugin { ]); initInfraServer(this.libs); - registerAlertTypes(plugins.alerts, this.libs); + registerAlertTypes(plugins.alerts, this.libs, plugins.ml); core.http.registerRouteHandlerContext( 'infra', diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index cc2cf4092520a..3da560135eaf4 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -9,17 +9,21 @@ import { PreviewResult } from '../../lib/alerting/common/types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + METRIC_ANOMALY_ALERT_TYPE_ID, INFRA_ALERT_PREVIEW_PATH, TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, alertPreviewRequestParamsRT, alertPreviewSuccessResponsePayloadRT, MetricThresholdAlertPreviewRequestParams, InventoryAlertPreviewRequestParams, + MetricAnomalyAlertPreviewRequestParams, } from '../../../common/alerting/metrics'; import { createValidationFunction } from '../../../common/runtime_types'; import { previewInventoryMetricThresholdAlert } from '../../lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert'; import { previewMetricThresholdAlert } from '../../lib/alerting/metric_threshold/preview_metric_threshold_alert'; +import { previewMetricAnomalyAlert } from '../../lib/alerting/metric_anomaly/preview_metric_anomaly_alert'; import { InfraBackendLibs } from '../../lib/infra_types'; +import { assertHasInfraMlPlugins } from '../../utils/request_context'; export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => { const { callWithRequest } = framework; @@ -33,8 +37,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }, framework.router.handleLegacyErrors(async (requestContext, request, response) => { const { - criteria, - filterQuery, lookback, sourceId, alertType, @@ -55,7 +57,11 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) try { switch (alertType) { case METRIC_THRESHOLD_ALERT_TYPE_ID: { - const { groupBy } = request.body as MetricThresholdAlertPreviewRequestParams; + const { + groupBy, + criteria, + filterQuery, + } = request.body as MetricThresholdAlertPreviewRequestParams; const previewResult = await previewMetricThresholdAlert({ callCluster, params: { criteria, filterQuery, groupBy }, @@ -72,7 +78,11 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }); } case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: { - const { nodeType } = request.body as InventoryAlertPreviewRequestParams; + const { + nodeType, + criteria, + filterQuery, + } = request.body as InventoryAlertPreviewRequestParams; const previewResult = await previewInventoryMetricThresholdAlert({ callCluster, params: { criteria, filterQuery, nodeType }, @@ -89,6 +99,39 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) body: alertPreviewSuccessResponsePayloadRT.encode(payload), }); } + case METRIC_ANOMALY_ALERT_TYPE_ID: { + assertHasInfraMlPlugins(requestContext); + const { + nodeType, + metric, + threshold, + influencerFilter, + } = request.body as MetricAnomalyAlertPreviewRequestParams; + const { mlAnomalyDetectors, mlSystem, spaceId } = requestContext.infra; + + const previewResult = await previewMetricAnomalyAlert({ + mlAnomalyDetectors, + mlSystem, + spaceId, + params: { nodeType, metric, threshold, influencerFilter }, + lookback, + sourceId: source.id, + alertInterval, + alertThrottle, + alertOnNoData, + }); + + return response.ok({ + body: alertPreviewSuccessResponsePayloadRT.encode({ + numberOfGroups: 1, + resultTotals: { + ...previewResult, + error: 0, + noData: 0, + }, + }), + }); + } default: throw new Error('Unknown alert type'); } diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts index 8ec0b83994e1a..6e227cfc12d11 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts @@ -53,7 +53,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { hasMoreEntries, timing, } = await getMetricsHostsAnomalies( - requestContext, + requestContext.infra, sourceId, anomalyThreshold, startTime, diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts index d41fa0ffafecc..1c2c4947a02ea 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts @@ -52,7 +52,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => { hasMoreEntries, timing, } = await getMetricK8sAnomalies( - requestContext, + requestContext.infra, sourceId, anomalyThreshold, startTime, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6e9d0329eaff8..018d2d572eea0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9676,7 +9676,6 @@ "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", "xpack.infra.alerting.alertsButton": "アラート", - "xpack.infra.alerting.createAlertButton": "アラートの作成", "xpack.infra.alerting.logs.alertsButton": "アラート", "xpack.infra.alerting.logs.createAlertButton": "アラートの作成", "xpack.infra.alerting.logs.manageAlerts": "アラートを管理", @@ -9970,16 +9969,6 @@ "xpack.infra.logs.jumpToTailText": "最も新しいエントリーに移動", "xpack.infra.logs.lastUpdate": "前回の更新 {timestamp}", "xpack.infra.logs.loadingNewEntriesText": "新しいエントリーを読み込み中", - "xpack.infra.logs.logAnalysis.splash.learnMoreLink": "ドキュメンテーションを表示", - "xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "詳細について", - "xpack.infra.logs.logAnalysis.splash.loadingMessage": "ライセンスを確認しています...", - "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "プレースホルダー画像", - "xpack.infra.logs.logAnalysis.splash.startTrialCta": "トライアルを開始", - "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。", - "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "異常検知を利用するには、無料の試用版を開始してください", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "サブスクリプションのアップグレード", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "機械学習機能を使用するには、プラチナサブスクリプションが必要です。", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "異常検知を利用するには、プラチナサブスクリプションにアップグレードしてください", "xpack.infra.logs.logEntryActionsDetailsButton": "詳細を表示", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index eeda709104479..5a9695b8ddc3d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9702,7 +9702,6 @@ "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", "xpack.infra.alerting.alertsButton": "告警", - "xpack.infra.alerting.createAlertButton": "创建告警", "xpack.infra.alerting.logs.alertsButton": "告警", "xpack.infra.alerting.logs.createAlertButton": "创建告警", "xpack.infra.alerting.logs.manageAlerts": "管理告警", @@ -9997,16 +9996,6 @@ "xpack.infra.logs.jumpToTailText": "跳到最近的条目", "xpack.infra.logs.lastUpdate": "上次更新时间 {timestamp}", "xpack.infra.logs.loadingNewEntriesText": "正在加载新条目", - "xpack.infra.logs.logAnalysis.splash.learnMoreLink": "阅读文档", - "xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "希望了解详情?", - "xpack.infra.logs.logAnalysis.splash.loadingMessage": "正在检查许可证......", - "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "占位符图像", - "xpack.infra.logs.logAnalysis.splash.startTrialCta": "开始试用", - "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "我们的免费试用版包含 Machine Learning 功能,可用于检测日志中的异常。", - "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "要访问异常检测,请启动免费试用版", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "升级订阅", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "必须具有白金级订阅,才能使用 Machine Learning 功能。", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "要访问异常检测,请升级到白金级订阅", "xpack.infra.logs.logEntryActionsDetailsButton": "查看详情", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。", From 82f03282325367c075495baf9811694ced22ebe5 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 9 Feb 2021 18:59:47 -0500 Subject: [PATCH 086/100] Prefix with / (#90836) --- .../server/es_client/monitoring_endpoint_disable_watches.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts b/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts index 454379d17848e..8e9e8d916e496 100644 --- a/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts +++ b/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts @@ -13,7 +13,7 @@ export function monitoringEndpointDisableWatches(Client: any, _config: any, comp params: {}, urls: [ { - fmt: '_monitoring/migrate/alerts', + fmt: '/_monitoring/migrate/alerts', }, ], method: 'POST', From 4bab95229d91bcc416cd2f719e1301d7f7522496 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 9 Feb 2021 16:09:02 -0800 Subject: [PATCH 087/100] [dev-utils/ci-stats] support disabling ship errors (#90851) Co-authored-by: spalger --- .ci/Jenkinsfile_security_cypress | 3 ++- .ci/es-snapshots/Jenkinsfile_verify_es | 5 ++++- .../src/ci_stats_reporter/ship_ci_stats_cli.ts | 12 ++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.ci/Jenkinsfile_security_cypress b/.ci/Jenkinsfile_security_cypress index d7f702a56563f..f7b02cd1c4ab1 100644 --- a/.ci/Jenkinsfile_security_cypress +++ b/.ci/Jenkinsfile_security_cypress @@ -10,7 +10,8 @@ kibanaPipeline(timeoutMinutes: 180) { ) { catchError { withEnv([ - 'CI_PARALLEL_PROCESS_NUMBER=1' + 'CI_PARALLEL_PROCESS_NUMBER=1', + 'IGNORE_SHIP_CI_STATS_ERROR=true', ]) { def job = 'xpack-securityCypress' diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index b40cd91a45c57..736a71b73d14d 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -26,7 +26,10 @@ kibanaPipeline(timeoutMinutes: 150) { message: "[${SNAPSHOT_VERSION}] ES Snapshot Verification Failure", ) { retryable.enable(2) - withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { + withEnv([ + "ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}", + 'IGNORE_SHIP_CI_STATS_ERROR=true', + ]) { parallel([ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts index 1ee78518bb801..4d07b54b8cf03 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts @@ -22,10 +22,18 @@ export function shipCiStatsCli() { throw createFlagError('expected --metrics to be a string'); } + const maybeFail = (message: string) => { + const error = createFailError(message); + if (process.env.IGNORE_SHIP_CI_STATS_ERROR === 'true') { + error.exitCode = 0; + } + return error; + }; + const reporter = CiStatsReporter.fromEnv(log); if (!reporter.isEnabled()) { - throw createFailError('unable to initilize the CI Stats reporter'); + throw maybeFail('unable to initilize the CI Stats reporter'); } for (const path of metricPaths) { @@ -35,7 +43,7 @@ export function shipCiStatsCli() { if (await reporter.metrics(JSON.parse(json))) { log.success('shipped metrics from', path); } else { - throw createFailError('failed to ship metrics'); + throw maybeFail('failed to ship metrics'); } } }, From 8166becc5555f132636bc1e8662370d1b4bf7b6a Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 9 Feb 2021 17:15:12 -0700 Subject: [PATCH 088/100] Revert "[Metrics UI] Add Metrics Anomaly Alert Type (#89244)" This reverts commit 0d94968df1b7e9d3fc3d24abcc7797ba70983090. --- .../infra/common/alerting/metrics/types.ts | 43 +-- .../infra/common/infra_ml/anomaly_results.ts | 56 ++- .../common/components/alert_preview.tsx | 3 +- .../common/components/get_alert_preview.ts | 4 +- .../components/metrics_alert_dropdown.tsx | 151 --------- .../inventory/components/alert_dropdown.tsx | 59 ++++ .../inventory/components/alert_flyout.tsx | 18 +- .../inventory/components/node_type.tsx | 2 +- .../components/alert_flyout.tsx | 53 --- .../components/expression.test.tsx | 74 ---- .../metric_anomaly/components/expression.tsx | 320 ------------------ .../components/influencer_filter.tsx | 193 ----------- .../metric_anomaly/components/node_type.tsx | 117 ------- .../components/severity_threshold.tsx | 140 -------- .../metric_anomaly/components/validation.tsx | 35 -- .../public/alerting/metric_anomaly/index.ts | 46 --- .../components/alert_dropdown.tsx | 57 ++++ .../components/alert_flyout.tsx | 11 +- .../logging/log_analysis_setup/index.ts | 1 + .../subscription_splash_content.tsx | 176 ++++++++++ .../source_configuration_settings.tsx | 4 +- .../containers/ml/infra_ml_capabilities.tsx | 4 +- .../containers/with_kuery_autocompletion.tsx | 11 +- .../log_entry_categories/page_content.tsx | 2 +- .../logs/log_entry_rate/page_content.tsx | 2 +- .../infra/public/pages/metrics/index.tsx | 8 +- ...lyout.tsx => anomoly_detection_flyout.tsx} | 0 .../ml/anomaly_detection/flyout_home.tsx | 6 +- .../subscription_splash_content.tsx | 110 +++--- .../metrics_explorer/components/kuery_bar.tsx | 21 +- x-pack/plugins/infra/public/plugin.ts | 2 - x-pack/plugins/infra/public/types.ts | 3 +- .../metric_anomaly/evaluate_condition.ts | 51 --- .../metric_anomaly/metric_anomaly_executor.ts | 142 -------- .../preview_metric_anomaly_alert.ts | 120 ------- .../register_metric_anomaly_alert_type.ts | 110 ------ .../lib/alerting/register_alert_types.ts | 10 +- .../infra/server/lib/infra_ml/common.ts | 17 - .../infra/server/lib/infra_ml/index.ts | 1 - .../lib/infra_ml/metrics_hosts_anomalies.ts | 39 ++- .../lib/infra_ml/metrics_k8s_anomalies.ts | 39 ++- .../server/lib/infra_ml/queries/common.ts | 32 -- .../queries/metrics_hosts_anomalies.ts | 12 +- .../infra_ml/queries/metrics_k8s_anomalies.ts | 12 +- x-pack/plugins/infra/server/plugin.ts | 2 +- .../infra/server/routes/alerting/preview.ts | 51 +-- .../results/metrics_hosts_anomalies.ts | 2 +- .../infra_ml/results/metrics_k8s_anomalies.ts | 2 +- .../translations/translations/ja-JP.json | 11 + .../translations/translations/zh-CN.json | 11 + 50 files changed, 479 insertions(+), 1917 deletions(-) delete mode 100644 x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx create mode 100644 x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx rename x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/{anomaly_detection_flyout.tsx => anomoly_detection_flyout.tsx} (100%) rename x-pack/plugins/infra/public/{components => pages/metrics/inventory_view/components/ml/anomaly_detection}/subscription_splash_content.tsx (58%) delete mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts delete mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts delete mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts delete mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 7a4edb8f49189..a89f82e931fd4 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -4,15 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import * as rt from 'io-ts'; -import { ANOMALY_THRESHOLD } from '../../infra_ml'; import { ItemTypeRT } from '../../inventory_models/types'; // TODO: Have threshold and inventory alerts import these types from this file instead of from their // local directories export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; -export const METRIC_ANOMALY_ALERT_TYPE_ID = 'metrics.alert.anomaly'; export enum Comparator { GT = '>', @@ -35,26 +34,6 @@ export enum Aggregators { P99 = 'p99', } -const metricAnomalyNodeTypeRT = rt.union([rt.literal('hosts'), rt.literal('k8s')]); -const metricAnomalyMetricRT = rt.union([ - rt.literal('memory_usage'), - rt.literal('network_in'), - rt.literal('network_out'), -]); -const metricAnomalyInfluencerFilterRT = rt.type({ - fieldName: rt.string, - fieldValue: rt.string, -}); - -export interface MetricAnomalyParams { - nodeType: rt.TypeOf; - metric: rt.TypeOf; - alertInterval?: string; - sourceId?: string; - threshold: Exclude; - influencerFilter: rt.TypeOf | undefined; -} - // Alert Preview API const baseAlertRequestParamsRT = rt.intersection([ rt.partial({ @@ -72,6 +51,7 @@ const baseAlertRequestParamsRT = rt.intersection([ rt.literal('M'), rt.literal('y'), ]), + criteria: rt.array(rt.any), alertInterval: rt.string, alertThrottle: rt.string, alertOnNoData: rt.boolean, @@ -85,7 +65,6 @@ const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([ }), rt.type({ alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID), - criteria: rt.array(rt.any), }), ]); export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf< @@ -97,33 +76,15 @@ const inventoryAlertPreviewRequestParamsRT = rt.intersection([ rt.type({ nodeType: ItemTypeRT, alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID), - criteria: rt.array(rt.any), }), ]); export type InventoryAlertPreviewRequestParams = rt.TypeOf< typeof inventoryAlertPreviewRequestParamsRT >; -const metricAnomalyAlertPreviewRequestParamsRT = rt.intersection([ - baseAlertRequestParamsRT, - rt.type({ - nodeType: metricAnomalyNodeTypeRT, - metric: metricAnomalyMetricRT, - threshold: rt.number, - alertType: rt.literal(METRIC_ANOMALY_ALERT_TYPE_ID), - }), - rt.partial({ - influencerFilter: metricAnomalyInfluencerFilterRT, - }), -]); -export type MetricAnomalyAlertPreviewRequestParams = rt.TypeOf< - typeof metricAnomalyAlertPreviewRequestParamsRT ->; - export const alertPreviewRequestParamsRT = rt.union([ metricThresholdAlertPreviewRequestParamsRT, inventoryAlertPreviewRequestParamsRT, - metricAnomalyAlertPreviewRequestParamsRT, ]); export type AlertPreviewRequestParams = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts index 81e46d85ba220..589e57a1388b5 100644 --- a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts +++ b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts @@ -5,44 +5,36 @@ * 2.0. */ -export enum ANOMALY_SEVERITY { - CRITICAL = 'critical', - MAJOR = 'major', - MINOR = 'minor', - WARNING = 'warning', - LOW = 'low', - UNKNOWN = 'unknown', -} +export const ML_SEVERITY_SCORES = { + warning: 3, + minor: 25, + major: 50, + critical: 75, +}; -export enum ANOMALY_THRESHOLD { - CRITICAL = 75, - MAJOR = 50, - MINOR = 25, - WARNING = 3, - LOW = 0, -} +export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES; -export const SEVERITY_COLORS = { - CRITICAL: '#fe5050', - MAJOR: '#fba740', - MINOR: '#fdec25', - WARNING: '#8bc8fb', - LOW: '#d2e9f7', - BLANK: '#ffffff', +export const ML_SEVERITY_COLORS = { + critical: 'rgb(228, 72, 72)', + major: 'rgb(229, 113, 0)', + minor: 'rgb(255, 221, 0)', + warning: 'rgb(125, 180, 226)', }; -export const getSeverityCategoryForScore = (score: number): ANOMALY_SEVERITY | undefined => { - if (score >= ANOMALY_THRESHOLD.CRITICAL) { - return ANOMALY_SEVERITY.CRITICAL; - } else if (score >= ANOMALY_THRESHOLD.MAJOR) { - return ANOMALY_SEVERITY.MAJOR; - } else if (score >= ANOMALY_THRESHOLD.MINOR) { - return ANOMALY_SEVERITY.MINOR; - } else if (score >= ANOMALY_THRESHOLD.WARNING) { - return ANOMALY_SEVERITY.WARNING; +export const getSeverityCategoryForScore = ( + score: number +): MLSeverityScoreCategories | undefined => { + if (score >= ML_SEVERITY_SCORES.critical) { + return 'critical'; + } else if (score >= ML_SEVERITY_SCORES.major) { + return 'major'; + } else if (score >= ML_SEVERITY_SCORES.minor) { + return 'minor'; + } else if (score >= ML_SEVERITY_SCORES.warning) { + return 'warning'; } else { // Category is too low to include - return ANOMALY_SEVERITY.LOW; + return undefined; } }; diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index 0135e185e846a..fac87e20dfe7d 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -37,7 +37,7 @@ interface Props { alertInterval: string; alertThrottle: string; alertType: PreviewableAlertTypes; - alertParams: { criteria?: any[]; sourceId: string } & Record; + alertParams: { criteria: any[]; sourceId: string } & Record; validate: (params: any) => ValidationResult; showNoDataResults?: boolean; groupByDisplayName?: string; @@ -109,7 +109,6 @@ export const AlertPreview: React.FC = (props) => { }, [previewLookbackInterval, alertInterval]); const isPreviewDisabled = useMemo(() => { - if (!alertParams.criteria) return false; const validationResult = validate({ criteria: alertParams.criteria } as any); const hasValidationErrors = Object.values(validationResult.errors).some((result) => Object.values(result).some((arr) => Array.isArray(arr) && arr.length) diff --git a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts index 2bb98e83cbe70..a1cee1361a18f 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts +++ b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts @@ -10,15 +10,13 @@ import { INFRA_ALERT_PREVIEW_PATH, METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - METRIC_ANOMALY_ALERT_TYPE_ID, AlertPreviewRequestParams, AlertPreviewSuccessResponsePayload, } from '../../../../common/alerting/metrics'; export type PreviewableAlertTypes = | typeof METRIC_THRESHOLD_ALERT_TYPE_ID - | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID - | typeof METRIC_ANOMALY_ALERT_TYPE_ID; + | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID; export async function getAlertPreview({ fetch, diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx deleted file mode 100644 index f1236c4fc2c2b..0000000000000 --- a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx +++ /dev/null @@ -1,151 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import React, { useState, useCallback, useMemo } from 'react'; -import { - EuiPopover, - EuiButtonEmpty, - EuiContextMenu, - EuiContextMenuPanelDescriptor, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities'; -import { PrefilledInventoryAlertFlyout } from '../../inventory/components/alert_flyout'; -import { PrefilledThresholdAlertFlyout } from '../../metric_threshold/components/alert_flyout'; -import { PrefilledAnomalyAlertFlyout } from '../../metric_anomaly/components/alert_flyout'; -import { useLinkProps } from '../../../hooks/use_link_props'; - -type VisibleFlyoutType = 'inventory' | 'threshold' | 'anomaly' | null; - -export const MetricsAlertDropdown = () => { - const [popoverOpen, setPopoverOpen] = useState(false); - const [visibleFlyoutType, setVisibleFlyoutType] = useState(null); - const { hasInfraMLCapabilities } = useInfraMLCapabilities(); - - const closeFlyout = useCallback(() => setVisibleFlyoutType(null), [setVisibleFlyoutType]); - - const manageAlertsLinkProps = useLinkProps({ - app: 'management', - pathname: '/insightsAndAlerting/triggersActions/alerts', - }); - - const panels: EuiContextMenuPanelDescriptor[] = useMemo( - () => [ - { - id: 0, - title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', { - defaultMessage: 'Alerts', - }), - items: [ - { - name: i18n.translate('xpack.infra.alerting.infrastructureDropdownMenu', { - defaultMessage: 'Infrastructure', - }), - panel: 1, - }, - { - name: i18n.translate('xpack.infra.alerting.metricsDropdownMenu', { - defaultMessage: 'Metrics', - }), - panel: 2, - }, - { - name: i18n.translate('xpack.infra.alerting.manageAlerts', { - defaultMessage: 'Manage alerts', - }), - icon: 'tableOfContents', - onClick: manageAlertsLinkProps.onClick, - }, - ], - }, - { - id: 1, - title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', { - defaultMessage: 'Infrastructure alerts', - }), - items: [ - { - name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', { - defaultMessage: 'Create inventory alert', - }), - onClick: () => setVisibleFlyoutType('inventory'), - }, - ].concat( - hasInfraMLCapabilities - ? { - name: i18n.translate('xpack.infra.alerting.createAnomalyAlertButton', { - defaultMessage: 'Create anomaly alert', - }), - onClick: () => setVisibleFlyoutType('anomaly'), - } - : [] - ), - }, - { - id: 2, - title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', { - defaultMessage: 'Metrics alerts', - }), - items: [ - { - name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', { - defaultMessage: 'Create threshold alert', - }), - onClick: () => setVisibleFlyoutType('threshold'), - }, - ], - }, - ], - [manageAlertsLinkProps, setVisibleFlyoutType, hasInfraMLCapabilities] - ); - - const closePopover = useCallback(() => { - setPopoverOpen(false); - }, [setPopoverOpen]); - - const openPopover = useCallback(() => { - setPopoverOpen(true); - }, [setPopoverOpen]); - - return ( - <> - - - - } - isOpen={popoverOpen} - closePopover={closePopover} - > - - - - - ); -}; - -interface AlertFlyoutProps { - visibleFlyoutType: VisibleFlyoutType; - onClose(): void; -} - -const AlertFlyout = ({ visibleFlyoutType, onClose }: AlertFlyoutProps) => { - switch (visibleFlyoutType) { - case 'inventory': - return ; - case 'threshold': - return ; - case 'anomaly': - return ; - default: - return null; - } -}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx new file mode 100644 index 0000000000000..a7b6c9fb7104c --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; +import { AlertFlyout } from './alert_flyout'; +import { ManageAlertsContextMenuItem } from './manage_alerts_context_menu_item'; + +export const InventoryAlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [flyoutVisible, setFlyoutVisible] = useState(false); + + const { inventoryPrefill } = useAlertPrefillContext(); + const { nodeType, metric, filterQuery } = inventoryPrefill; + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + const menuItems = [ + setFlyoutVisible(true)}> + + , + , + ]; + + return ( + <> + + + + } + isOpen={popoverOpen} + closePopover={closePopover} + > + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index 33fe3c7af30c7..815e1f2be33f2 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -8,7 +8,8 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; import { InfraWaffleMapOptions } from '../../../lib/lib'; import { InventoryItemType } from '../../../../common/inventory_models/types'; import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; @@ -48,18 +49,3 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: return <>{visible && AddAlertFlyout}; }; - -export const PrefilledInventoryAlertFlyout = ({ onClose }: { onClose(): void }) => { - const { inventoryPrefill } = useAlertPrefillContext(); - const { nodeType, metric, filterQuery } = inventoryPrefill; - - return ( - - ); -}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx index bd7812acac678..f02f98c49f01a 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx @@ -68,7 +68,7 @@ export const NodeTypeExpression = ({ setAggTypePopoverOpen(false)}> { - const { triggersActionsUI } = useContext(TriggerActionsContext); - - const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]); - const AddAlertFlyout = useMemo( - () => - triggersActionsUI && - triggersActionsUI.getAddAlertFlyout({ - consumer: 'infrastructure', - onClose: onCloseFlyout, - canChangeTrigger: false, - alertTypeId: METRIC_ANOMALY_ALERT_TYPE_ID, - metadata: { - metric, - nodeType, - }, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [triggersActionsUI, visible] - ); - - return <>{visible && AddAlertFlyout}; -}; - -export const PrefilledAnomalyAlertFlyout = ({ onClose }: { onClose(): void }) => { - const { inventoryPrefill } = useAlertPrefillContext(); - const { nodeType, metric } = inventoryPrefill; - - return ; -}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx deleted file mode 100644 index ae2c6ed81badb..0000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx +++ /dev/null @@ -1,74 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mountWithIntl, nextTick } from '@kbn/test/jest'; -// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` -import { coreMock as mockCoreMock } from 'src/core/public/mocks'; -import React from 'react'; -import { Expression, AlertContextMeta } from './expression'; -import { act } from 'react-dom/test-utils'; - -jest.mock('../../../containers/source/use_source_via_http', () => ({ - useSourceViaHttp: () => ({ - source: { id: 'default' }, - createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), - }), -})); - -jest.mock('../../../hooks/use_kibana', () => ({ - useKibanaContextForPlugin: () => ({ - services: mockCoreMock.createStart(), - }), -})); - -jest.mock('../../../containers/ml/infra_ml_capabilities', () => ({ - useInfraMLCapabilities: () => ({ - isLoading: false, - hasInfraMLCapabilities: true, - }), -})); - -describe('Expression', () => { - async function setup(currentOptions: AlertContextMeta) { - const alertParams = { - metric: undefined, - nodeType: undefined, - threshold: 50, - }; - const wrapper = mountWithIntl( - Reflect.set(alertParams, key, value)} - setAlertProperty={() => {}} - metadata={currentOptions} - /> - ); - - const update = async () => - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - await update(); - - return { wrapper, update, alertParams }; - } - - it('should prefill the alert using the context metadata', async () => { - const currentOptions = { - nodeType: 'pod', - metric: { type: 'tx' }, - }; - const { alertParams } = await setup(currentOptions as AlertContextMeta); - expect(alertParams.nodeType).toBe('k8s'); - expect(alertParams.metric).toBe('network_out'); - }); -}); diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx deleted file mode 100644 index 5938c7119616f..0000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx +++ /dev/null @@ -1,320 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pick } from 'lodash'; -import React, { useCallback, useState, useMemo, useEffect } from 'react'; -import { EuiFlexGroup, EuiSpacer, EuiText, EuiLoadingContent } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities'; -import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; -import { AlertPreview } from '../../common'; -import { - METRIC_ANOMALY_ALERT_TYPE_ID, - MetricAnomalyParams, -} from '../../../../common/alerting/metrics'; -import { euiStyled, EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; -import { - WhenExpression, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../triggers_actions_ui/public/common'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; -import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; -import { findInventoryModel } from '../../../../common/inventory_models'; -import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { NodeTypeExpression } from './node_type'; -import { SeverityThresholdExpression } from './severity_threshold'; -import { InfraWaffleMapOptions } from '../../../lib/lib'; -import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; - -import { validateMetricAnomaly } from './validation'; -import { InfluencerFilter } from './influencer_filter'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; - -export interface AlertContextMeta { - metric?: InfraWaffleMapOptions['metric']; - nodeType?: InventoryItemType; -} - -interface Props { - errors: IErrorObject[]; - alertParams: MetricAnomalyParams & { - sourceId: string; - }; - alertInterval: string; - alertThrottle: string; - setAlertParams(key: string, value: any): void; - setAlertProperty(key: string, value: any): void; - metadata: AlertContextMeta; -} - -export const defaultExpression = { - metric: 'memory_usage' as MetricAnomalyParams['metric'], - threshold: ANOMALY_THRESHOLD.MAJOR, - nodeType: 'hosts', - influencerFilter: undefined, -}; - -export const Expression: React.FC = (props) => { - const { hasInfraMLCapabilities, isLoading: isLoadingMLCapabilities } = useInfraMLCapabilities(); - const { http, notifications } = useKibanaContextForPlugin().services; - const { setAlertParams, alertParams, alertInterval, alertThrottle, metadata } = props; - const { source, createDerivedIndexPattern } = useSourceViaHttp({ - sourceId: 'default', - type: 'metrics', - fetch: http.fetch, - toastWarning: notifications.toasts.addWarning, - }); - - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ - createDerivedIndexPattern, - ]); - - const [influencerFieldName, updateInfluencerFieldName] = useState( - alertParams.influencerFilter?.fieldName ?? 'host.name' - ); - - useEffect(() => { - setAlertParams('hasInfraMLCapabilities', hasInfraMLCapabilities); - }, [setAlertParams, hasInfraMLCapabilities]); - - useEffect(() => { - if (alertParams.influencerFilter) { - setAlertParams('influencerFilter', { - ...alertParams.influencerFilter, - fieldName: influencerFieldName, - }); - } - }, [influencerFieldName, alertParams, setAlertParams]); - const updateInfluencerFieldValue = useCallback( - (value: string) => { - if (value) { - setAlertParams('influencerFilter', { - ...alertParams.influencerFilter, - fieldValue: value, - }); - } else { - setAlertParams('influencerFilter', undefined); - } - }, - [setAlertParams, alertParams] - ); - - useEffect(() => { - setAlertParams('alertInterval', alertInterval); - }, [setAlertParams, alertInterval]); - - const updateNodeType = useCallback( - (nt: any) => { - setAlertParams('nodeType', nt); - }, - [setAlertParams] - ); - - const updateMetric = useCallback( - (metric: string) => { - setAlertParams('metric', metric); - }, - [setAlertParams] - ); - - const updateSeverityThreshold = useCallback( - (threshold: any) => { - setAlertParams('threshold', threshold); - }, - [setAlertParams] - ); - - const prefillNodeType = useCallback(() => { - const md = metadata; - if (md && md.nodeType) { - setAlertParams( - 'nodeType', - getMLNodeTypeFromInventoryNodeType(md.nodeType) ?? defaultExpression.nodeType - ); - } else { - setAlertParams('nodeType', defaultExpression.nodeType); - } - }, [metadata, setAlertParams]); - - const prefillMetric = useCallback(() => { - const md = metadata; - if (md && md.metric) { - setAlertParams( - 'metric', - getMLMetricFromInventoryMetric(md.metric.type) ?? defaultExpression.metric - ); - } else { - setAlertParams('metric', defaultExpression.metric); - } - }, [metadata, setAlertParams]); - - useEffect(() => { - if (!alertParams.nodeType) { - prefillNodeType(); - } - - if (!alertParams.threshold) { - setAlertParams('threshold', defaultExpression.threshold); - } - - if (!alertParams.metric) { - prefillMetric(); - } - - if (!alertParams.sourceId) { - setAlertParams('sourceId', source?.id || 'default'); - } - }, [metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps - - if (isLoadingMLCapabilities) return ; - if (!hasInfraMLCapabilities) return ; - - return ( - // https://github.com/elastic/kibana/issues/89506 - - -

    - -

    -
    - - - - - - - - - - - - - - - - - - - -
    - ); -}; - -// required for dynamic import -// eslint-disable-next-line import/no-default-export -export default Expression; - -const StyledExpressionRow = euiStyled(EuiFlexGroup)` - display: flex; - flex-wrap: wrap; - margin: 0 -4px; -`; - -const StyledExpression = euiStyled.div` - padding: 0 4px; -`; - -const getDisplayNameForType = (type: InventoryItemType) => { - const inventoryModel = findInventoryModel(type); - return inventoryModel.displayName; -}; - -export const nodeTypes: { [key: string]: any } = { - hosts: { - text: getDisplayNameForType('host'), - value: 'hosts', - }, - k8s: { - text: getDisplayNameForType('pod'), - value: 'k8s', - }, -}; - -const getMLMetricFromInventoryMetric = (metric: SnapshotMetricType) => { - switch (metric) { - case 'memory': - return 'memory_usage'; - case 'tx': - return 'network_out'; - case 'rx': - return 'network_in'; - default: - return null; - } -}; - -const getMLNodeTypeFromInventoryNodeType = (nodeType: InventoryItemType) => { - switch (nodeType) { - case 'host': - return 'hosts'; - case 'pod': - return 'k8s'; - default: - return null; - } -}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx deleted file mode 100644 index 34a917a77dcf5..0000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx +++ /dev/null @@ -1,193 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { debounce } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import React, { useState, useCallback, useEffect, useMemo } from 'react'; -import { first } from 'lodash'; -import { EuiFlexGroup, EuiFormRow, EuiCheckbox, EuiFlexItem, EuiSelect } from '@elastic/eui'; -import { - MetricsExplorerKueryBar, - CurryLoadSuggestionsType, -} from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; -import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; - -interface Props { - fieldName: string; - fieldValue: string; - nodeType: MetricAnomalyParams['nodeType']; - onChangeFieldName: (v: string) => void; - onChangeFieldValue: (v: string) => void; - derivedIndexPattern: Parameters[0]['derivedIndexPattern']; -} - -const FILTER_TYPING_DEBOUNCE_MS = 500; - -export const InfluencerFilter = ({ - fieldName, - fieldValue, - nodeType, - onChangeFieldName, - onChangeFieldValue, - derivedIndexPattern, -}: Props) => { - const fieldNameOptions = useMemo(() => (nodeType === 'k8s' ? k8sFieldNames : hostFieldNames), [ - nodeType, - ]); - - // If initial props contain a fieldValue, assume it was passed in from loaded alertParams, - // and enable the UI element - const [isEnabled, updateIsEnabled] = useState(fieldValue ? true : false); - const [storedFieldValue, updateStoredFieldValue] = useState(fieldValue); - - useEffect( - () => - nodeType === 'k8s' - ? onChangeFieldName(first(k8sFieldNames)!.value) - : onChangeFieldName(first(hostFieldNames)!.value), - [nodeType, onChangeFieldName] - ); - - const onSelectFieldName = useCallback((e) => onChangeFieldName(e.target.value), [ - onChangeFieldName, - ]); - const onUpdateFieldValue = useCallback( - (value) => { - updateStoredFieldValue(value); - onChangeFieldValue(value); - }, - [onChangeFieldValue] - ); - - const toggleEnabled = useCallback(() => { - const nextState = !isEnabled; - updateIsEnabled(nextState); - if (!nextState) { - onChangeFieldValue(''); - } else { - onChangeFieldValue(storedFieldValue); - } - }, [isEnabled, updateIsEnabled, onChangeFieldValue, storedFieldValue]); - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - const debouncedOnUpdateFieldValue = useCallback( - debounce(onUpdateFieldValue, FILTER_TYPING_DEBOUNCE_MS), - [onUpdateFieldValue] - ); - - const affixFieldNameToQuery: CurryLoadSuggestionsType = (fn) => ( - expression, - cursorPosition, - maxSuggestions - ) => { - // Add the field name to the front of the passed-in query - const prefix = `${fieldName}:`; - // Trim whitespace to prevent AND/OR suggestions - const modifiedExpression = `${prefix}${expression}`.trim(); - // Move the cursor position forward by the length of the field name - const modifiedPosition = cursorPosition + prefix.length; - return fn(modifiedExpression, modifiedPosition, maxSuggestions, (suggestions) => - suggestions - .map((s) => ({ - ...s, - // Remove quotes from suggestions - text: s.text.replace(/\"/g, '').trim(), - // Offset the returned suggestions' cursor positions so that they can be autocompleted accurately - start: s.start - prefix.length, - end: s.end - prefix.length, - })) - // Removing quotes can lead to an already-selected suggestion still coming up in the autocomplete list, - // so filter these out - .filter((s) => !expression.startsWith(s.text)) - ); - }; - - return ( - - } - helpText={ - isEnabled ? ( - <> - {i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpText', { - defaultMessage: - 'Limit the scope of your alert trigger to anomalies influenced by certain node(s).', - })} -
    - {i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpTextExample', { - defaultMessage: 'For example: "my-node-1" or "my-node-*"', - })} - - ) : null - } - fullWidth - display="rowCompressed" - > - {isEnabled ? ( - - - - - - - - - ) : ( - <> - )} -
    - ); -}; - -const hostFieldNames = [ - { - value: 'host.name', - text: 'host.name', - }, -]; - -const k8sFieldNames = [ - { - value: 'kubernetes.pod.uid', - text: 'kubernetes.pod.uid', - }, - { - value: 'kubernetes.node.name', - text: 'kubernetes.node.name', - }, - { - value: 'kubernetes.namespace', - text: 'kubernetes.namespace', - }, -]; - -const filterByNodeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.filterByNodeLabel', { - defaultMessage: 'Filter by node', -}); diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx deleted file mode 100644 index 6ddcf8fd5cb65..0000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; -import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; -import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; - -type Node = MetricAnomalyParams['nodeType']; - -interface WhenExpressionProps { - value: Node; - options: { [key: string]: { text: string; value: Node } }; - onChange: (value: Node) => void; - popupPosition?: - | 'upCenter' - | 'upLeft' - | 'upRight' - | 'downCenter' - | 'downLeft' - | 'downRight' - | 'leftCenter' - | 'leftUp' - | 'leftDown' - | 'rightCenter' - | 'rightUp' - | 'rightDown'; -} - -export const NodeTypeExpression = ({ - value, - options, - onChange, - popupPosition, -}: WhenExpressionProps) => { - const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); - - return ( - { - setAggTypePopoverOpen(true); - }} - /> - } - isOpen={aggTypePopoverOpen} - closePopover={() => { - setAggTypePopoverOpen(false); - }} - ownFocus - anchorPosition={popupPosition ?? 'downLeft'} - > -
    - setAggTypePopoverOpen(false)}> - - - { - onChange(e.target.value as Node); - setAggTypePopoverOpen(false); - }} - options={Object.values(options).map((o) => o)} - /> -
    -
    - ); -}; - -interface ClosablePopoverTitleProps { - children: JSX.Element; - onClose: () => void; -} - -export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { - return ( - - - {children} - - onClose()} - /> - - - - ); -}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx deleted file mode 100644 index 2dc561ff172b9..0000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx +++ /dev/null @@ -1,140 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; -import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; -import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; - -interface WhenExpressionProps { - value: Exclude; - onChange: (value: ANOMALY_THRESHOLD) => void; - popupPosition?: - | 'upCenter' - | 'upLeft' - | 'upRight' - | 'downCenter' - | 'downLeft' - | 'downRight' - | 'leftCenter' - | 'leftUp' - | 'leftDown' - | 'rightCenter' - | 'rightUp' - | 'rightDown'; -} - -const options = { - [ANOMALY_THRESHOLD.CRITICAL]: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.criticalLabel', { - defaultMessage: 'Critical', - }), - value: ANOMALY_THRESHOLD.CRITICAL, - }, - [ANOMALY_THRESHOLD.MAJOR]: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.majorLabel', { - defaultMessage: 'Major', - }), - value: ANOMALY_THRESHOLD.MAJOR, - }, - [ANOMALY_THRESHOLD.MINOR]: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.minorLabel', { - defaultMessage: 'Minor', - }), - value: ANOMALY_THRESHOLD.MINOR, - }, - [ANOMALY_THRESHOLD.WARNING]: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.warningLabel', { - defaultMessage: 'Warning', - }), - value: ANOMALY_THRESHOLD.WARNING, - }, -}; - -export const SeverityThresholdExpression = ({ - value, - onChange, - popupPosition, -}: WhenExpressionProps) => { - const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); - - return ( - { - setAggTypePopoverOpen(true); - }} - /> - } - isOpen={aggTypePopoverOpen} - closePopover={() => { - setAggTypePopoverOpen(false); - }} - ownFocus - anchorPosition={popupPosition ?? 'downLeft'} - > -
    - setAggTypePopoverOpen(false)}> - - - { - onChange(Number(e.target.value) as ANOMALY_THRESHOLD); - setAggTypePopoverOpen(false); - }} - options={Object.values(options).map((o) => o)} - /> -
    -
    - ); -}; - -interface ClosablePopoverTitleProps { - children: JSX.Element; - onClose: () => void; -} - -export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { - return ( - - - {children} - - onClose()} - /> - - - - ); -}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx deleted file mode 100644 index 8e254fb2b67a8..0000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx +++ /dev/null @@ -1,35 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; - -export function validateMetricAnomaly({ - hasInfraMLCapabilities, -}: { - hasInfraMLCapabilities: boolean; -}): ValidationResult { - const validationResult = { errors: {} }; - const errors: { - hasInfraMLCapabilities: string[]; - } = { - hasInfraMLCapabilities: [], - }; - - validationResult.errors = errors; - - if (!hasInfraMLCapabilities) { - errors.hasInfraMLCapabilities.push( - i18n.translate('xpack.infra.metrics.alertFlyout.error.mlCapabilitiesRequired', { - defaultMessage: 'Cannot create an anomaly alert when machine learning is disabled.', - }) - ); - } - - return validationResult; -} diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts deleted file mode 100644 index 31fed514bdacc..0000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts +++ /dev/null @@ -1,46 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../common/alerting/metrics'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { AlertTypeParams } from '../../../../alerts/common'; -import { validateMetricAnomaly } from './components/validation'; - -interface MetricAnomalyAlertTypeParams extends AlertTypeParams { - hasInfraMLCapabilities: boolean; -} - -export function createMetricAnomalyAlertType(): AlertTypeModel { - return { - id: METRIC_ANOMALY_ALERT_TYPE_ID, - description: i18n.translate('xpack.infra.metrics.anomaly.alertFlyout.alertDescription', { - defaultMessage: 'Alert when the anomaly score exceeds a defined threshold.', - }), - iconClass: 'bell', - documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/metric-anomaly-alert.html`; - }, - alertParamsExpression: React.lazy(() => import('./components/expression')), - validate: validateMetricAnomaly, - defaultActionMessage: i18n.translate( - 'xpack.infra.metrics.alerting.anomaly.defaultActionMessage', - { - defaultMessage: `\\{\\{alertName\\}\\} is in a state of \\{\\{context.alertState\\}\\} - -\\{\\{context.metric\\}\\} was \\{\\{context.summary\\}\\} than normal at \\{\\{context.timestamp\\}\\} - -Typical value: \\{\\{context.typical\\}\\} -Actual value: \\{\\{context.actual\\}\\} -`, - } - ), - requiresAppContext: false, - }; -} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx new file mode 100644 index 0000000000000..3bbe811225825 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useAlertPrefillContext } from '../../use_alert_prefill'; +import { AlertFlyout } from './alert_flyout'; +import { ManageAlertsContextMenuItem } from '../../inventory/components/manage_alerts_context_menu_item'; + +export const MetricsAlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [flyoutVisible, setFlyoutVisible] = useState(false); + + const { metricThresholdPrefill } = useAlertPrefillContext(); + const { groupBy, filterQuery, metrics } = metricThresholdPrefill; + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + const menuItems = [ + setFlyoutVisible(true)}> + + , + , + ]; + + return ( + <> + + + + } + isOpen={popoverOpen} + closePopover={closePopover} + > + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index e7e4ade5257fc..929654ecb4693 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -7,10 +7,10 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; -import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; interface Props { visible?: boolean; @@ -42,10 +42,3 @@ export const AlertFlyout = (props: Props) => { return <>{visible && AddAlertFlyout}; }; - -export const PrefilledThresholdAlertFlyout = ({ onClose }: { onClose(): void }) => { - const { metricThresholdPrefill } = useAlertPrefillContext(); - const { groupBy, filterQuery, metrics } = metricThresholdPrefill; - - return ; -}; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts index db5a996c604fc..1bcc9e7157a51 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts @@ -14,3 +14,4 @@ export * from './missing_results_privileges_prompt'; export * from './missing_setup_privileges_prompt'; export * from './ml_unavailable_prompt'; export * from './setup_status_unknown_prompt'; +export * from './subscription_splash_content'; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx new file mode 100644 index 0000000000000..c91c1d82afe9b --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiText, + EuiButton, + EuiButtonEmpty, + EuiImage, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { HttpStart } from 'src/core/public'; +import { LoadingPage } from '../../loading_page'; + +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { useTrialStatus } from '../../../hooks/use_trial_status'; + +export const SubscriptionSplashContent: React.FC = () => { + const { services } = useKibana<{ http: HttpStart }>(); + const { loadState, isTrialAvailable, checkTrialAvailability } = useTrialStatus(); + + useEffect(() => { + checkTrialAvailability(); + }, [checkTrialAvailability]); + + if (loadState === 'pending') { + return ( + + ); + } + + const canStartTrial = isTrialAvailable && loadState === 'resolved'; + + let title; + let description; + let cta; + + if (canStartTrial) { + title = ( + + ); + + description = ( + + ); + + cta = ( + + + + ); + } else { + title = ( + + ); + + description = ( + + ); + + cta = ( + + + + ); + } + + return ( + + + + + + +

    {title}

    +
    + + +

    {description}

    +
    + +
    {cta}
    +
    + + + +
    + + +

    + +

    +
    + + + +
    +
    +
    +
    + ); +}; + +const SubscriptionPage = euiStyled(EuiPage)` + height: 100% +`; + +const SubscriptionPageContent = euiStyled(EuiPageContent)` + max-width: 768px !important; +`; + +const SubscriptionPageFooter = euiStyled.div` + background: ${(props) => props.theme.eui.euiColorLightestShade}; + margin: 0 -${(props) => props.theme.eui.paddingSizes.l} -${(props) => + props.theme.eui.paddingSizes.l}; + padding: ${(props) => props.theme.eui.paddingSizes.l}; +`; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index e63f43470497d..4b609a881bd18 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -75,7 +75,7 @@ export const SourceConfigurationSettings = ({ source, ]); - const { hasInfraMLCapabilities } = useInfraMLCapabilitiesContext(); + const { hasInfraMLCapabilites } = useInfraMLCapabilitiesContext(); if ((isLoading || isUninitialized) && !source) { return ; @@ -128,7 +128,7 @@ export const SourceConfigurationSettings = ({ /> - {hasInfraMLCapabilities && ( + {hasInfraMLCapabilites && ( <> { const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob; const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs; - const hasInfraMLCapabilities = + const hasInfraMLCapabilites = mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace; return { - hasInfraMLCapabilities, + hasInfraMLCapabilites, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities, isLoading, diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx index 1a759950f640d..379ac9774c242 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -56,8 +56,7 @@ class WithKueryAutocompletionComponent extends React.Component< private loadSuggestions = async ( expression: string, cursorPosition: number, - maxSuggestions?: number, - transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[] + maxSuggestions?: number ) => { const { indexPattern } = this.props; const language = 'kuery'; @@ -87,10 +86,6 @@ class WithKueryAutocompletionComponent extends React.Component< boolFilter: [], })) || []; - const transformedSuggestions = transformSuggestions - ? transformSuggestions(suggestions) - : suggestions; - this.setState((state) => state.currentRequest && state.currentRequest.expression !== expression && @@ -99,9 +94,7 @@ class WithKueryAutocompletionComponent extends React.Component< : { ...state, currentRequest: null, - suggestions: maxSuggestions - ? transformedSuggestions.slice(0, maxSuggestions) - : transformedSuggestions, + suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions, } ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index 628df397998ee..f0fdd79bcd93d 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback, useEffect } from 'react'; -import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, + SubscriptionSplashContent, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index 5fd00527b8b70..4d06d23ef93ef 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import React, { memo, useEffect, useCallback } from 'react'; -import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, + SubscriptionSplashContent, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 8fd32bda7fbc8..52c2a70f2d359 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -35,11 +35,12 @@ import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; -import { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown'; +import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown'; +import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; import { SavedView } from '../../containers/saved_view/saved_view'; import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities'; -import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout'; +import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout'; import { HeaderMenuPortal } from '../../../../observability/public'; import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; @@ -82,7 +83,8 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
    - + + { jobSummaries: k8sJobSummaries, } = useMetricK8sModuleContext(); const { - hasInfraMLCapabilities, + hasInfraMLCapabilites, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities, } = useInfraMLCapabilitiesContext(); @@ -69,7 +69,7 @@ export const FlyoutHome = (props: Props) => { } }, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]); - if (!hasInfraMLCapabilities) { + if (!hasInfraMLCapabilites) { return ; } else if (!hasInfraMLReadCapabilities) { return ; diff --git a/x-pack/plugins/infra/public/components/subscription_splash_content.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx similarity index 58% rename from x-pack/plugins/infra/public/components/subscription_splash_content.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx index a6477dfc7d172..e05759ab57dd5 100644 --- a/x-pack/plugins/infra/public/components/subscription_splash_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx @@ -22,11 +22,11 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { euiStyled, EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; -import { HttpStart } from '../../../../../src/core/public'; -import { useTrialStatus } from '../hooks/use_trial_status'; -import { LoadingPage } from '../components/loading_page'; +import { LoadingPage } from '../../../../../../components/loading_page'; +import { useTrialStatus } from '../../../../../../hooks/use_trial_status'; +import { useKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; +import { HttpStart } from '../../../../../../../../../../src/core/public'; export const SubscriptionSplashContent: React.FC = () => { const { services } = useKibana<{ http: HttpStart }>(); @@ -102,60 +102,58 @@ export const SubscriptionSplashContent: React.FC = () => { } return ( - - - - - - - -

    {title}

    -
    - - -

    {description}

    -
    - -
    {cta}
    -
    - - - -
    - - -

    - -

    + + + + + + +

    {title}

    - + + +

    {description}

    +
    + +
    {cta}
    +
    + + + +
    + + +

    - - - - - - +

    +
    + + + +
    +
    +
    +
    ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx index e22c6fa661181..44391568741f3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx @@ -10,19 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../../../../components/autocomplete_field'; -import { - esKuery, - IIndexPattern, - QuerySuggestion, -} from '../../../../../../../../src/plugins/data/public'; - -type LoadSuggestionsFn = ( - e: string, - p: number, - m?: number, - transform?: (s: QuerySuggestion[]) => QuerySuggestion[] -) => void; -export type CurryLoadSuggestionsType = (loadSuggestions: LoadSuggestionsFn) => LoadSuggestionsFn; +import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; interface Props { derivedIndexPattern: IIndexPattern; @@ -30,7 +18,6 @@ interface Props { onChange?: (query: string) => void; value?: string | null; placeholder?: string; - curryLoadSuggestions?: CurryLoadSuggestionsType; } function validateQuery(query: string) { @@ -48,7 +35,6 @@ export const MetricsExplorerKueryBar = ({ onChange, value, placeholder, - curryLoadSuggestions = defaultCurryLoadSuggestions, }: Props) => { const [draftQuery, setDraftQuery] = useState(value || ''); const [isValid, setValidation] = useState(true); @@ -87,7 +73,7 @@ export const MetricsExplorerKueryBar = ({ aria-label={placeholder} isLoadingSuggestions={isLoadingSuggestions} isValid={isValid} - loadSuggestions={curryLoadSuggestions(loadSuggestions)} + loadSuggestions={loadSuggestions} onChange={handleChange} onSubmit={onSubmit} placeholder={placeholder || defaultPlaceholder} @@ -98,6 +84,3 @@ export const MetricsExplorerKueryBar = ({ ); }; - -const defaultCurryLoadSuggestions: CurryLoadSuggestionsType = (loadSuggestions) => (...args) => - loadSuggestions(...args); diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index d4bb83e8668ba..8e7d165f8a535 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -10,7 +10,6 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { createMetricThresholdAlertType } from './alerting/metric_threshold'; import { createInventoryMetricAlertType } from './alerting/inventory'; -import { createMetricAnomalyAlertType } from './alerting/metric_anomaly'; import { getAlertType as getLogsAlertType } from './alerting/log_threshold'; import { registerFeatures } from './register_feature'; import { @@ -36,7 +35,6 @@ export class Plugin implements InfraClientPluginClass { pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType()); pluginsSetup.triggersActionsUi.alertTypeRegistry.register(getLogsAlertType()); pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType()); - pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricAnomalyAlertType()); if (pluginsSetup.observability) { pluginsSetup.observability.dashboard.register({ diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 4d70676d25e40..b18b6e8a6eba6 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -23,7 +23,7 @@ import type { ObservabilityPluginStart, } from '../../observability/public'; import type { SpacesPluginStart } from '../../spaces/public'; -import { MlPluginStart, MlPluginSetup } from '../../ml/public'; +import { MlPluginStart } from '../../ml/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; // Our own setup and start contract values @@ -36,7 +36,6 @@ export interface InfraClientSetupDeps { observability: ObservabilityPluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionSetup; - ml: MlPluginSetup; embeddable: EmbeddableSetup; } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts deleted file mode 100644 index b7ef8ec7d2312..0000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts +++ /dev/null @@ -1,51 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; -import { getMetricsHostsAnomalies, getMetricK8sAnomalies } from '../../infra_ml'; -import { MlSystem, MlAnomalyDetectors } from '../../../types'; - -type ConditionParams = Omit & { - spaceId: string; - startTime: number; - endTime: number; - mlSystem: MlSystem; - mlAnomalyDetectors: MlAnomalyDetectors; -}; - -export const evaluateCondition = async ({ - nodeType, - spaceId, - sourceId, - mlSystem, - mlAnomalyDetectors, - startTime, - endTime, - metric, - threshold, - influencerFilter, -}: ConditionParams) => { - const getAnomalies = nodeType === 'k8s' ? getMetricK8sAnomalies : getMetricsHostsAnomalies; - - const result = await getAnomalies( - { - spaceId, - mlSystem, - mlAnomalyDetectors, - }, - sourceId ?? 'default', - threshold, - startTime, - endTime, - metric, - { field: 'anomalyScore', direction: 'desc' }, - { pageSize: 100 }, - influencerFilter - ); - - return result; -}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts deleted file mode 100644 index ec95aac7268ad..0000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts +++ /dev/null @@ -1,142 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { first } from 'lodash'; -import moment from 'moment'; -import { stateToAlertMessage } from '../common/messages'; -import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; -import { MappedAnomalyHit } from '../../infra_ml'; -import { AlertStates } from '../common/types'; -import { - ActionGroup, - AlertInstanceContext, - AlertInstanceState, -} from '../../../../../alerts/common'; -import { AlertExecutorOptions } from '../../../../../alerts/server'; -import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; -import { MetricAnomalyAllowedActionGroups } from './register_metric_anomaly_alert_type'; -import { MlPluginSetup } from '../../../../../ml/server'; -import { KibanaRequest } from '../../../../../../../src/core/server'; -import { InfraBackendLibs } from '../../infra_types'; -import { evaluateCondition } from './evaluate_condition'; - -export const createMetricAnomalyExecutor = (libs: InfraBackendLibs, ml?: MlPluginSetup) => async ({ - services, - params, - startedAt, -}: AlertExecutorOptions< - /** - * TODO: Remove this use of `any` by utilizing a proper type - */ - Record, - Record, - AlertInstanceState, - AlertInstanceContext, - MetricAnomalyAllowedActionGroups ->) => { - if (!ml) { - return; - } - const request = {} as KibanaRequest; - const mlSystem = ml.mlSystemProvider(request, services.savedObjectsClient); - const mlAnomalyDetectors = ml.anomalyDetectorsProvider(request, services.savedObjectsClient); - - const { - metric, - alertInterval, - influencerFilter, - sourceId, - nodeType, - threshold, - } = params as MetricAnomalyParams; - - const alertInstance = services.alertInstanceFactory(`${nodeType}-${metric}`); - - const bucketInterval = getIntervalInSeconds('15m') * 1000; - const alertIntervalInMs = getIntervalInSeconds(alertInterval ?? '1m') * 1000; - - const endTime = startedAt.getTime(); - // Anomalies are bucketed at :00, :15, :30, :45 minutes every hour - const previousBucketStartTime = endTime - (endTime % bucketInterval); - - // If the alert interval is less than 15m, make sure that it actually queries an anomaly bucket - const startTime = Math.min(endTime - alertIntervalInMs, previousBucketStartTime); - - const { data } = await evaluateCondition({ - sourceId: sourceId ?? 'default', - spaceId: 'default', - mlSystem, - mlAnomalyDetectors, - startTime, - endTime, - metric, - threshold, - nodeType, - influencerFilter, - }); - - const shouldAlertFire = data.length > 0; - - if (shouldAlertFire) { - const { startTime: anomalyStartTime, anomalyScore, actual, typical, influencers } = first( - data as MappedAnomalyHit[] - )!; - - alertInstance.scheduleActions(FIRED_ACTIONS_ID, { - alertState: stateToAlertMessage[AlertStates.ALERT], - timestamp: moment(anomalyStartTime).toISOString(), - anomalyScore, - actual, - typical, - metric: metricNameMap[metric], - summary: generateSummaryMessage(actual, typical), - influencers: influencers.join(', '), - }); - } -}; - -export const FIRED_ACTIONS_ID = 'metrics.anomaly.fired'; -export const FIRED_ACTIONS: ActionGroup = { - id: FIRED_ACTIONS_ID, - name: i18n.translate('xpack.infra.metrics.alerting.anomaly.fired', { - defaultMessage: 'Fired', - }), -}; - -const generateSummaryMessage = (actual: number, typical: number) => { - const differential = (Math.max(actual, typical) / Math.min(actual, typical)) - .toFixed(1) - .replace('.0', ''); - if (actual > typical) { - return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryHigher', { - defaultMessage: '{differential}x higher', - values: { - differential, - }, - }); - } else { - return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryLower', { - defaultMessage: '{differential}x lower', - values: { - differential, - }, - }); - } -}; - -const metricNameMap = { - memory_usage: i18n.translate('xpack.infra.metrics.alerting.anomaly.memoryUsage', { - defaultMessage: 'Memory usage', - }), - network_in: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkIn', { - defaultMessage: 'Network in', - }), - network_out: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkOut', { - defaultMessage: 'Network out', - }), -}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts deleted file mode 100644 index 98992701e3bb4..0000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts +++ /dev/null @@ -1,120 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Unit } from '@elastic/datemath'; -import { countBy } from 'lodash'; -import { MappedAnomalyHit } from '../../infra_ml'; -import { MlSystem, MlAnomalyDetectors } from '../../../types'; -import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; -import { - TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, - isTooManyBucketsPreviewException, -} from '../../../../common/alerting/metrics'; -import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; -import { evaluateCondition } from './evaluate_condition'; - -interface PreviewMetricAnomalyAlertParams { - mlSystem: MlSystem; - mlAnomalyDetectors: MlAnomalyDetectors; - spaceId: string; - params: MetricAnomalyParams; - sourceId: string; - lookback: Unit; - alertInterval: string; - alertThrottle: string; - alertOnNoData: boolean; -} - -export const previewMetricAnomalyAlert = async ({ - mlSystem, - mlAnomalyDetectors, - spaceId, - params, - sourceId, - lookback, - alertInterval, - alertThrottle, -}: PreviewMetricAnomalyAlertParams) => { - const { metric, threshold, influencerFilter, nodeType } = params as MetricAnomalyParams; - - const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); - const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle); - const executionsPerThrottle = Math.floor(throttleIntervalInSeconds / alertIntervalInSeconds); - - const lookbackInterval = `1${lookback}`; - const lookbackIntervalInSeconds = getIntervalInSeconds(lookbackInterval); - const endTime = Date.now(); - const startTime = endTime - lookbackIntervalInSeconds * 1000; - - const numberOfExecutions = Math.floor(lookbackIntervalInSeconds / alertIntervalInSeconds); - const bucketIntervalInSeconds = getIntervalInSeconds('15m'); - const bucketsPerExecution = Math.max( - 1, - Math.floor(alertIntervalInSeconds / bucketIntervalInSeconds) - ); - - try { - let anomalies: MappedAnomalyHit[] = []; - const { data } = await evaluateCondition({ - nodeType, - spaceId, - sourceId, - mlSystem, - mlAnomalyDetectors, - startTime, - endTime, - metric, - threshold, - influencerFilter, - }); - anomalies = [...anomalies, ...data]; - - const anomaliesByTime = countBy(anomalies, ({ startTime: anomStartTime }) => anomStartTime); - - let numberOfTimesFired = 0; - let numberOfNotifications = 0; - let throttleTracker = 0; - const notifyWithThrottle = () => { - if (throttleTracker === 0) numberOfNotifications++; - throttleTracker++; - }; - // Mock each alert evaluation - for (let i = 0; i < numberOfExecutions; i++) { - const executionTime = startTime + alertIntervalInSeconds * 1000 * i; - // Get an array of bucket times this mock alert evaluation will be looking at - // Anomalies are bucketed at :00, :15, :30, :45 minutes every hour, - // so this is an array of how many of those times occurred between this evaluation - // and the previous one - const bucketsLookedAt = Array.from(Array(bucketsPerExecution), (_, idx) => { - const previousBucketStartTime = - executionTime - - (executionTime % (bucketIntervalInSeconds * 1000)) - - idx * bucketIntervalInSeconds * 1000; - return previousBucketStartTime; - }); - const anomaliesDetectedInBuckets = bucketsLookedAt.some((bucketTime) => - Reflect.has(anomaliesByTime, bucketTime) - ); - - if (anomaliesDetectedInBuckets) { - numberOfTimesFired++; - notifyWithThrottle(); - } else if (throttleTracker > 0) { - throttleTracker++; - } - if (throttleTracker === executionsPerThrottle) { - throttleTracker = 0; - } - } - - return { fired: numberOfTimesFired, notifications: numberOfNotifications }; - } catch (e) { - if (!isTooManyBucketsPreviewException(e)) throw e; - const { maxBuckets } = e; - throw new Error(`${TOO_MANY_BUCKETS_PREVIEW_EXCEPTION}:${maxBuckets}`); - } -}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts deleted file mode 100644 index 8ac62c125515a..0000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts +++ /dev/null @@ -1,110 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import { i18n } from '@kbn/i18n'; -import { MlPluginSetup } from '../../../../../ml/server'; -import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../../alerts/server'; -import { - createMetricAnomalyExecutor, - FIRED_ACTIONS, - FIRED_ACTIONS_ID, -} from './metric_anomaly_executor'; -import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; -import { InfraBackendLibs } from '../../infra_types'; -import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils'; -import { alertStateActionVariableDescription } from '../common/messages'; -import { RecoveredActionGroupId } from '../../../../../alerts/common'; - -export type MetricAnomalyAllowedActionGroups = typeof FIRED_ACTIONS_ID; - -export const registerMetricAnomalyAlertType = ( - libs: InfraBackendLibs, - ml?: MlPluginSetup -): AlertType< - /** - * TODO: Remove this use of `any` by utilizing a proper type - */ - Record, - Record, - AlertInstanceState, - AlertInstanceContext, - MetricAnomalyAllowedActionGroups, - RecoveredActionGroupId -> => ({ - id: METRIC_ANOMALY_ALERT_TYPE_ID, - name: i18n.translate('xpack.infra.metrics.anomaly.alertName', { - defaultMessage: 'Infrastructure anomaly', - }), - validate: { - params: schema.object( - { - nodeType: oneOfLiterals(['hosts', 'k8s']), - alertInterval: schema.string(), - metric: oneOfLiterals(['memory_usage', 'network_in', 'network_out']), - threshold: schema.number(), - filterQuery: schema.maybe( - schema.string({ validate: validateIsStringElasticsearchJSONFilter }) - ), - sourceId: schema.string(), - }, - { unknowns: 'allow' } - ), - }, - defaultActionGroupId: FIRED_ACTIONS_ID, - actionGroups: [FIRED_ACTIONS], - producer: 'infrastructure', - minimumLicenseRequired: 'basic', - executor: createMetricAnomalyExecutor(libs, ml), - actionVariables: { - context: [ - { name: 'alertState', description: alertStateActionVariableDescription }, - { - name: 'metric', - description: i18n.translate('xpack.infra.metrics.alerting.anomalyMetricDescription', { - defaultMessage: 'The metric name in the specified condition.', - }), - }, - { - name: 'timestamp', - description: i18n.translate('xpack.infra.metrics.alerting.anomalyTimestampDescription', { - defaultMessage: 'A timestamp of when the anomaly was detected.', - }), - }, - { - name: 'anomalyScore', - description: i18n.translate('xpack.infra.metrics.alerting.anomalyScoreDescription', { - defaultMessage: 'The exact severity score of the detected anomaly.', - }), - }, - { - name: 'actual', - description: i18n.translate('xpack.infra.metrics.alerting.anomalyActualDescription', { - defaultMessage: 'The actual value of the monitored metric at the time of the anomaly.', - }), - }, - { - name: 'typical', - description: i18n.translate('xpack.infra.metrics.alerting.anomalyTypicalDescription', { - defaultMessage: 'The typical value of the monitored metric at the time of the anomaly.', - }), - }, - { - name: 'summary', - description: i18n.translate('xpack.infra.metrics.alerting.anomalySummaryDescription', { - defaultMessage: 'A description of the anomaly, e.g. "2x higher."', - }), - }, - { - name: 'influencers', - description: i18n.translate('xpack.infra.metrics.alerting.anomalyInfluencersDescription', { - defaultMessage: 'A list of node names that influenced the anomaly.', - }), - }, - ], - }, -}); diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index 11fbe269b854d..0b4df6805759e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -8,21 +8,13 @@ import { PluginSetupContract } from '../../../../alerts/server'; import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type'; -import { registerMetricAnomalyAlertType } from './metric_anomaly/register_metric_anomaly_alert_type'; - import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type'; import { InfraBackendLibs } from '../infra_types'; -import { MlPluginSetup } from '../../../../ml/server'; -const registerAlertTypes = ( - alertingPlugin: PluginSetupContract, - libs: InfraBackendLibs, - ml?: MlPluginSetup -) => { +const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => { if (alertingPlugin) { alertingPlugin.registerType(registerMetricThresholdAlertType(libs)); alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs)); - alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml)); const registerFns = [registerLogThresholdAlertType]; registerFns.forEach((fn) => { diff --git a/x-pack/plugins/infra/server/lib/infra_ml/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/common.ts index 686f27d714cc1..0182cb0e4099a 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/common.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/common.ts @@ -17,23 +17,6 @@ import { import { decodeOrThrow } from '../../../common/runtime_types'; import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing'; -export interface MappedAnomalyHit { - id: string; - anomalyScore: number; - typical: number; - actual: number; - jobId: string; - startTime: number; - duration: number; - influencers: string[]; - categoryId?: string; -} - -export interface InfluencerFilter { - fieldName: string; - fieldValue: string; -} - export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) { const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES'); const { diff --git a/x-pack/plugins/infra/server/lib/infra_ml/index.ts b/x-pack/plugins/infra/server/lib/infra_ml/index.ts index 82093b1a359d0..d346b71d76aa8 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/index.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/index.ts @@ -8,4 +8,3 @@ export * from './errors'; export * from './metrics_hosts_anomalies'; export * from './metrics_k8s_anomalies'; -export { MappedAnomalyHit } from './common'; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts index f6e11f5294191..7873fd8e43a7b 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts @@ -5,10 +5,11 @@ * 2.0. */ +import type { InfraPluginRequestHandlerContext } from '../../types'; import { InfraRequestHandlerContext } from '../../types'; import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common'; -import { getJobId, metricsHostsJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml'; +import { fetchMlJob } from './common'; +import { getJobId, metricsHostsJobTypes } from '../../../common/infra_ml'; import { Sort, Pagination } from '../../../common/http_api/infra_ml'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; @@ -18,6 +19,18 @@ import { createMetricsHostsAnomaliesQuery, } from './queries/metrics_hosts_anomalies'; +interface MappedAnomalyHit { + id: string; + anomalyScore: number; + typical: number; + actual: number; + jobId: string; + startTime: number; + duration: number; + influencers: string[]; + categoryId?: string; +} + async function getCompatibleAnomaliesJobIds( spaceId: string, sourceId: string, @@ -61,15 +74,14 @@ async function getCompatibleAnomaliesJobIds( } export async function getMetricsHostsAnomalies( - context: Required, + context: InfraPluginRequestHandlerContext & { infra: Required }, sourceId: string, - anomalyThreshold: ANOMALY_THRESHOLD, + anomalyThreshold: number, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, sort: Sort, - pagination: Pagination, - influencerFilter?: InfluencerFilter + pagination: Pagination ) { const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies'); @@ -77,10 +89,10 @@ export async function getMetricsHostsAnomalies( jobIds, timing: { spans: jobSpans }, } = await getCompatibleAnomaliesJobIds( - context.spaceId, + context.infra.spaceId, sourceId, metric, - context.mlAnomalyDetectors + context.infra.mlAnomalyDetectors ); if (jobIds.length === 0) { @@ -96,14 +108,13 @@ export async function getMetricsHostsAnomalies( hasMoreEntries, timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricsHostsAnomalies( - context.mlSystem, + context.infra.mlSystem, anomalyThreshold, jobIds, startTime, endTime, sort, - pagination, - influencerFilter + pagination ); const data = anomalies.map((anomaly) => { @@ -153,13 +164,12 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricsHostsAnomalies( mlSystem: MlSystem, - anomalyThreshold: ANOMALY_THRESHOLD, + anomalyThreshold: number, jobIds: string[], startTime: number, endTime: number, sort: Sort, - pagination: Pagination, - influencerFilter?: InfluencerFilter + pagination: Pagination ) { // We'll request 1 extra entry on top of our pageSize to determine if there are // more entries to be fetched. This avoids scenarios where the client side can't @@ -178,7 +188,6 @@ async function fetchMetricsHostsAnomalies( endTime, sort, pagination: expandedPagination, - influencerFilter, }), jobIds ) diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts index 34039e9107f00..0c87b2f0f8b53 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts @@ -5,10 +5,11 @@ * 2.0. */ +import type { InfraPluginRequestHandlerContext } from '../../types'; import { InfraRequestHandlerContext } from '../../types'; import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common'; -import { getJobId, metricsK8SJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml'; +import { fetchMlJob } from './common'; +import { getJobId, metricsK8SJobTypes } from '../../../common/infra_ml'; import { Sort, Pagination } from '../../../common/http_api/infra_ml'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; @@ -18,6 +19,18 @@ import { createMetricsK8sAnomaliesQuery, } from './queries/metrics_k8s_anomalies'; +interface MappedAnomalyHit { + id: string; + anomalyScore: number; + typical: number; + actual: number; + jobId: string; + startTime: number; + influencers: string[]; + duration: number; + categoryId?: string; +} + async function getCompatibleAnomaliesJobIds( spaceId: string, sourceId: string, @@ -61,15 +74,14 @@ async function getCompatibleAnomaliesJobIds( } export async function getMetricK8sAnomalies( - context: Required, + context: InfraPluginRequestHandlerContext & { infra: Required }, sourceId: string, - anomalyThreshold: ANOMALY_THRESHOLD, + anomalyThreshold: number, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, sort: Sort, - pagination: Pagination, - influencerFilter?: InfluencerFilter + pagination: Pagination ) { const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies'); @@ -77,10 +89,10 @@ export async function getMetricK8sAnomalies( jobIds, timing: { spans: jobSpans }, } = await getCompatibleAnomaliesJobIds( - context.spaceId, + context.infra.spaceId, sourceId, metric, - context.mlAnomalyDetectors + context.infra.mlAnomalyDetectors ); if (jobIds.length === 0) { @@ -95,14 +107,13 @@ export async function getMetricK8sAnomalies( hasMoreEntries, timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricK8sAnomalies( - context.mlSystem, + context.infra.mlSystem, anomalyThreshold, jobIds, startTime, endTime, sort, - pagination, - influencerFilter + pagination ); const data = anomalies.map((anomaly) => { @@ -149,13 +160,12 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricK8sAnomalies( mlSystem: MlSystem, - anomalyThreshold: ANOMALY_THRESHOLD, + anomalyThreshold: number, jobIds: string[], startTime: number, endTime: number, sort: Sort, - pagination: Pagination, - influencerFilter?: InfluencerFilter | undefined + pagination: Pagination ) { // We'll request 1 extra entry on top of our pageSize to determine if there are // more entries to be fetched. This avoids scenarios where the client side can't @@ -174,7 +184,6 @@ async function fetchMetricK8sAnomalies( endTime, sort, pagination: expandedPagination, - influencerFilter, }), jobIds ) diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts index 6f996a672a44a..b3676fc54aeaa 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts @@ -77,35 +77,3 @@ export const createDatasetsFilters = (datasets?: string[]) => }, ] : []; - -export const createInfluencerFilter = ({ - fieldName, - fieldValue, -}: { - fieldName: string; - fieldValue: string; -}) => [ - { - nested: { - path: 'influencers', - query: { - bool: { - must: [ - { - match: { - 'influencers.influencer_field_name': fieldName, - }, - }, - { - query_string: { - fields: ['influencers.influencer_field_values'], - query: fieldValue, - minimum_should_match: 1, - }, - }, - ], - }, - }, - }, - }, -]; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts index 7808851508a7c..45587cd258e5d 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts @@ -6,7 +6,6 @@ */ import * as rt from 'io-ts'; -import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { createJobIdsFilters, @@ -14,9 +13,7 @@ import { createResultTypeFilters, defaultRequestParameters, createAnomalyScoreFilter, - createInfluencerFilter, } from './common'; -import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; // TODO: Reassess validity of this against ML docs @@ -35,15 +32,13 @@ export const createMetricsHostsAnomaliesQuery = ({ endTime, sort, pagination, - influencerFilter, }: { jobIds: string[]; - anomalyThreshold: ANOMALY_THRESHOLD; + anomalyThreshold: number; startTime: number; endTime: number; sort: Sort; pagination: Pagination; - influencerFilter?: InfluencerFilter; }) => { const { field } = sort; const { pageSize } = pagination; @@ -55,10 +50,6 @@ export const createMetricsHostsAnomaliesQuery = ({ ...createResultTypeFilters(['record']), ]; - const influencerQuery = influencerFilter - ? { must: createInfluencerFilter(influencerFilter) } - : {}; - const sourceFields = [ 'job_id', 'record_score', @@ -86,7 +77,6 @@ export const createMetricsHostsAnomaliesQuery = ({ query: { bool: { filter: filters, - ...influencerQuery, }, }, search_after: queryCursor, diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts index 54eea067177ed..56a4b99e7236c 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts @@ -6,7 +6,6 @@ */ import * as rt from 'io-ts'; -import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { createJobIdsFilters, @@ -14,9 +13,7 @@ import { createResultTypeFilters, defaultRequestParameters, createAnomalyScoreFilter, - createInfluencerFilter, } from './common'; -import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; // TODO: Reassess validity of this against ML docs @@ -35,15 +32,13 @@ export const createMetricsK8sAnomaliesQuery = ({ endTime, sort, pagination, - influencerFilter, }: { jobIds: string[]; - anomalyThreshold: ANOMALY_THRESHOLD; + anomalyThreshold: number; startTime: number; endTime: number; sort: Sort; pagination: Pagination; - influencerFilter?: InfluencerFilter; }) => { const { field } = sort; const { pageSize } = pagination; @@ -55,10 +50,6 @@ export const createMetricsK8sAnomaliesQuery = ({ ...createResultTypeFilters(['record']), ]; - const influencerQuery = influencerFilter - ? { must: createInfluencerFilter(influencerFilter) } - : {}; - const sourceFields = [ 'job_id', 'record_score', @@ -85,7 +76,6 @@ export const createMetricsK8sAnomaliesQuery = ({ query: { bool: { filter: filters, - ...influencerQuery, }, }, search_after: queryCursor, diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 0ac49e05b36b9..99555fa56acd5 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -137,7 +137,7 @@ export class InfraServerPlugin implements Plugin { ]); initInfraServer(this.libs); - registerAlertTypes(plugins.alerts, this.libs, plugins.ml); + registerAlertTypes(plugins.alerts, this.libs); core.http.registerRouteHandlerContext( 'infra', diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 3da560135eaf4..cc2cf4092520a 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -9,21 +9,17 @@ import { PreviewResult } from '../../lib/alerting/common/types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - METRIC_ANOMALY_ALERT_TYPE_ID, INFRA_ALERT_PREVIEW_PATH, TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, alertPreviewRequestParamsRT, alertPreviewSuccessResponsePayloadRT, MetricThresholdAlertPreviewRequestParams, InventoryAlertPreviewRequestParams, - MetricAnomalyAlertPreviewRequestParams, } from '../../../common/alerting/metrics'; import { createValidationFunction } from '../../../common/runtime_types'; import { previewInventoryMetricThresholdAlert } from '../../lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert'; import { previewMetricThresholdAlert } from '../../lib/alerting/metric_threshold/preview_metric_threshold_alert'; -import { previewMetricAnomalyAlert } from '../../lib/alerting/metric_anomaly/preview_metric_anomaly_alert'; import { InfraBackendLibs } from '../../lib/infra_types'; -import { assertHasInfraMlPlugins } from '../../utils/request_context'; export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => { const { callWithRequest } = framework; @@ -37,6 +33,8 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }, framework.router.handleLegacyErrors(async (requestContext, request, response) => { const { + criteria, + filterQuery, lookback, sourceId, alertType, @@ -57,11 +55,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) try { switch (alertType) { case METRIC_THRESHOLD_ALERT_TYPE_ID: { - const { - groupBy, - criteria, - filterQuery, - } = request.body as MetricThresholdAlertPreviewRequestParams; + const { groupBy } = request.body as MetricThresholdAlertPreviewRequestParams; const previewResult = await previewMetricThresholdAlert({ callCluster, params: { criteria, filterQuery, groupBy }, @@ -78,11 +72,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }); } case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: { - const { - nodeType, - criteria, - filterQuery, - } = request.body as InventoryAlertPreviewRequestParams; + const { nodeType } = request.body as InventoryAlertPreviewRequestParams; const previewResult = await previewInventoryMetricThresholdAlert({ callCluster, params: { criteria, filterQuery, nodeType }, @@ -99,39 +89,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) body: alertPreviewSuccessResponsePayloadRT.encode(payload), }); } - case METRIC_ANOMALY_ALERT_TYPE_ID: { - assertHasInfraMlPlugins(requestContext); - const { - nodeType, - metric, - threshold, - influencerFilter, - } = request.body as MetricAnomalyAlertPreviewRequestParams; - const { mlAnomalyDetectors, mlSystem, spaceId } = requestContext.infra; - - const previewResult = await previewMetricAnomalyAlert({ - mlAnomalyDetectors, - mlSystem, - spaceId, - params: { nodeType, metric, threshold, influencerFilter }, - lookback, - sourceId: source.id, - alertInterval, - alertThrottle, - alertOnNoData, - }); - - return response.ok({ - body: alertPreviewSuccessResponsePayloadRT.encode({ - numberOfGroups: 1, - resultTotals: { - ...previewResult, - error: 0, - noData: 0, - }, - }), - }); - } default: throw new Error('Unknown alert type'); } diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts index 6e227cfc12d11..8ec0b83994e1a 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts @@ -53,7 +53,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { hasMoreEntries, timing, } = await getMetricsHostsAnomalies( - requestContext.infra, + requestContext, sourceId, anomalyThreshold, startTime, diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts index 1c2c4947a02ea..d41fa0ffafecc 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts @@ -52,7 +52,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => { hasMoreEntries, timing, } = await getMetricK8sAnomalies( - requestContext.infra, + requestContext, sourceId, anomalyThreshold, startTime, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 018d2d572eea0..6e9d0329eaff8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9676,6 +9676,7 @@ "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", "xpack.infra.alerting.alertsButton": "アラート", + "xpack.infra.alerting.createAlertButton": "アラートの作成", "xpack.infra.alerting.logs.alertsButton": "アラート", "xpack.infra.alerting.logs.createAlertButton": "アラートの作成", "xpack.infra.alerting.logs.manageAlerts": "アラートを管理", @@ -9969,6 +9970,16 @@ "xpack.infra.logs.jumpToTailText": "最も新しいエントリーに移動", "xpack.infra.logs.lastUpdate": "前回の更新 {timestamp}", "xpack.infra.logs.loadingNewEntriesText": "新しいエントリーを読み込み中", + "xpack.infra.logs.logAnalysis.splash.learnMoreLink": "ドキュメンテーションを表示", + "xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "詳細について", + "xpack.infra.logs.logAnalysis.splash.loadingMessage": "ライセンスを確認しています...", + "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "プレースホルダー画像", + "xpack.infra.logs.logAnalysis.splash.startTrialCta": "トライアルを開始", + "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。", + "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "異常検知を利用するには、無料の試用版を開始してください", + "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "サブスクリプションのアップグレード", + "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "機械学習機能を使用するには、プラチナサブスクリプションが必要です。", + "xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "異常検知を利用するには、プラチナサブスクリプションにアップグレードしてください", "xpack.infra.logs.logEntryActionsDetailsButton": "詳細を表示", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5a9695b8ddc3d..eeda709104479 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9702,6 +9702,7 @@ "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", "xpack.infra.alerting.alertsButton": "告警", + "xpack.infra.alerting.createAlertButton": "创建告警", "xpack.infra.alerting.logs.alertsButton": "告警", "xpack.infra.alerting.logs.createAlertButton": "创建告警", "xpack.infra.alerting.logs.manageAlerts": "管理告警", @@ -9996,6 +9997,16 @@ "xpack.infra.logs.jumpToTailText": "跳到最近的条目", "xpack.infra.logs.lastUpdate": "上次更新时间 {timestamp}", "xpack.infra.logs.loadingNewEntriesText": "正在加载新条目", + "xpack.infra.logs.logAnalysis.splash.learnMoreLink": "阅读文档", + "xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "希望了解详情?", + "xpack.infra.logs.logAnalysis.splash.loadingMessage": "正在检查许可证......", + "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "占位符图像", + "xpack.infra.logs.logAnalysis.splash.startTrialCta": "开始试用", + "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "我们的免费试用版包含 Machine Learning 功能,可用于检测日志中的异常。", + "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "要访问异常检测,请启动免费试用版", + "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "升级订阅", + "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "必须具有白金级订阅,才能使用 Machine Learning 功能。", + "xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "要访问异常检测,请升级到白金级订阅", "xpack.infra.logs.logEntryActionsDetailsButton": "查看详情", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。", From 1ba7d7af9a36a48d7d956ae58d5acd0967b6ff0f Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 9 Feb 2021 17:31:57 -0700 Subject: [PATCH 089/100] [Maps] remove maps_file_upload plugin and fold public folder into file_upload plugin (#90292) * get geojson working with api/file_upload/import * remove maps_file_upload server code * remove common folder * remove maps_file_upload plugin * fix tsconfig paths * rename file_upload plugin in maps tsconfig * fix file path * node scripts/build_plugin_list_docs * fix webpack compile errors * telemetry schema cleanup, i18n cleanup, limits cleanup * remove mapsFileUpload from limits.yml * remove index pattern link test case * update telemetry/v2/clusters/_stats for new file_upload path Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/developer/plugin-list.asciidoc | 4 - packages/kbn-optimizer/limits.yml | 2 +- x-pack/.i18nrc.json | 2 +- x-pack/plugins/file_upload/kibana.json | 4 +- .../public/components/index_settings.js | 0 .../public/components/json_import_progress.js | 4 +- .../components/json_index_file_picker.js | 2 +- .../components/json_upload_and_parse.js | 0 .../public/get_file_upload_component.ts | 0 .../public/index.ts | 2 + .../public/kibana_services.js | 0 .../public/plugin.ts | 0 .../public/util/file_parser.js | 0 .../public/util/file_parser.test.js | 0 .../util/geo_json_clean_and_validate.js | 0 .../util/geo_json_clean_and_validate.test.js | 0 .../public/util/geo_processing.js | 37 ++-- .../public/util/geo_processing.test.js | 3 +- .../public/util/http_service.js | 0 .../public/util/indexing_service.js | 8 +- .../public/util/indexing_service.test.js | 0 .../public/util/size_limited_chunking.js | 2 +- .../public/util/size_limited_chunking.test.js | 0 x-pack/plugins/file_upload/tsconfig.json | 1 + x-pack/plugins/maps/kibana.json | 2 +- .../layers/file_upload_wizard/wizard.tsx | 2 +- x-pack/plugins/maps/public/kibana_services.ts | 2 +- x-pack/plugins/maps/public/plugin.ts | 4 +- x-pack/plugins/maps/tsconfig.json | 2 +- x-pack/plugins/maps_file_upload/README.md | 3 - .../common/constants/file_import.ts | 21 --- .../plugins/maps_file_upload/jest.config.js | 12 -- x-pack/plugins/maps_file_upload/kibana.json | 8 - x-pack/plugins/maps_file_upload/mappings.ts | 16 -- .../maps_file_upload/server/client/errors.js | 12 -- .../plugins/maps_file_upload/server/index.js | 12 -- .../server/kibana_server_services.js | 12 -- .../server/models/import_data/import_data.js | 161 ------------------ .../server/models/import_data/index.js | 8 - .../plugins/maps_file_upload/server/plugin.js | 27 --- .../server/routes/file_upload.js | 131 -------------- .../server/routes/file_upload.test.js | 77 --------- .../telemetry/file_upload_usage_collector.ts | 29 ---- .../server/telemetry/index.ts | 9 - .../server/telemetry/mappings.ts | 22 --- .../server/telemetry/telemetry.test.ts | 48 ------ .../server/telemetry/telemetry.ts | 62 ------- x-pack/plugins/maps_file_upload/tsconfig.json | 15 -- .../file_error_callouts.tsx | 2 +- .../file_based/components/utils/utils.ts | 2 +- .../schema/xpack_plugins.json | 7 - .../apis/telemetry/telemetry_local.js | 2 +- .../import_geojson/file_indexing_panel.js | 15 -- x-pack/tsconfig.json | 2 - x-pack/tsconfig.refs.json | 1 - 55 files changed, 36 insertions(+), 763 deletions(-) rename x-pack/plugins/{maps_file_upload => file_upload}/public/components/index_settings.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/components/json_import_progress.js (96%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/components/json_index_file_picker.js (99%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/components/json_upload_and_parse.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/get_file_upload_component.ts (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/index.ts (94%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/kibana_services.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/plugin.ts (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/file_parser.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/file_parser.test.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/geo_json_clean_and_validate.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/geo_json_clean_and_validate.test.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/geo_processing.js (76%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/geo_processing.test.js (97%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/http_service.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/indexing_service.js (96%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/indexing_service.test.js (100%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/size_limited_chunking.js (95%) rename x-pack/plugins/{maps_file_upload => file_upload}/public/util/size_limited_chunking.test.js (100%) delete mode 100644 x-pack/plugins/maps_file_upload/README.md delete mode 100644 x-pack/plugins/maps_file_upload/common/constants/file_import.ts delete mode 100644 x-pack/plugins/maps_file_upload/jest.config.js delete mode 100644 x-pack/plugins/maps_file_upload/kibana.json delete mode 100644 x-pack/plugins/maps_file_upload/mappings.ts delete mode 100644 x-pack/plugins/maps_file_upload/server/client/errors.js delete mode 100644 x-pack/plugins/maps_file_upload/server/index.js delete mode 100644 x-pack/plugins/maps_file_upload/server/kibana_server_services.js delete mode 100644 x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js delete mode 100644 x-pack/plugins/maps_file_upload/server/models/import_data/index.js delete mode 100644 x-pack/plugins/maps_file_upload/server/plugin.js delete mode 100644 x-pack/plugins/maps_file_upload/server/routes/file_upload.js delete mode 100644 x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js delete mode 100644 x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts delete mode 100644 x-pack/plugins/maps_file_upload/server/telemetry/index.ts delete mode 100644 x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts delete mode 100644 x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts delete mode 100644 x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts delete mode 100644 x-pack/plugins/maps_file_upload/tsconfig.json diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 6587d5dc422b4..263addc98ee62 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -443,10 +443,6 @@ using the CURL scripts in the scripts folder. |Visualize geo data from Elasticsearch or 3rd party geo-services. -|{kib-repo}blob/{branch}/x-pack/plugins/maps_file_upload/README.md[mapsFileUpload] -|Deprecated - plugin targeted for removal and will get merged into file_upload plugin - - |{kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] |This plugin provides access to the detailed tile map services from Elastic. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 490c2ccc19d7d..a1e40c06f6fa1 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -104,4 +104,4 @@ pageLoadAssetSize: presentationUtil: 28545 spacesOss: 18817 osquery: 107090 - mapsFileUpload: 23775 + fileUpload: 25664 diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index f95c4286b3f26..c09198b3874a1 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -20,7 +20,7 @@ "xpack.endpoint": "plugins/endpoint", "xpack.enterpriseSearch": "plugins/enterprise_search", "xpack.features": "plugins/features", - "xpack.fileUpload": "plugins/maps_file_upload", + "xpack.fileUpload": "plugins/file_upload", "xpack.globalSearch": ["plugins/global_search"], "xpack.globalSearchBar": ["plugins/global_search_bar"], "xpack.graph": ["plugins/graph"], diff --git a/x-pack/plugins/file_upload/kibana.json b/x-pack/plugins/file_upload/kibana.json index 7ca024174ec6a..7676a01d0b0f9 100644 --- a/x-pack/plugins/file_upload/kibana.json +++ b/x-pack/plugins/file_upload/kibana.json @@ -3,6 +3,6 @@ "version": "8.0.0", "kibanaVersion": "kibana", "server": true, - "ui": false, - "requiredPlugins": ["usageCollection"] + "ui": true, + "requiredPlugins": ["data", "usageCollection"] } diff --git a/x-pack/plugins/maps_file_upload/public/components/index_settings.js b/x-pack/plugins/file_upload/public/components/index_settings.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/components/index_settings.js rename to x-pack/plugins/file_upload/public/components/index_settings.js diff --git a/x-pack/plugins/maps_file_upload/public/components/json_import_progress.js b/x-pack/plugins/file_upload/public/components/json_import_progress.js similarity index 96% rename from x-pack/plugins/maps_file_upload/public/components/json_import_progress.js rename to x-pack/plugins/file_upload/public/components/json_import_progress.js index 535142bc3500e..1f9293e77d33c 100644 --- a/x-pack/plugins/maps_file_upload/public/components/json_import_progress.js +++ b/x-pack/plugins/file_upload/public/components/json_import_progress.js @@ -118,9 +118,7 @@ export class JsonImportProgress extends Component {
    {i18n.translate('xpack.fileUpload.jsonImport.indexMgmtLink', { defaultMessage: 'Index Management', diff --git a/x-pack/plugins/maps_file_upload/public/components/json_index_file_picker.js b/x-pack/plugins/file_upload/public/components/json_index_file_picker.js similarity index 99% rename from x-pack/plugins/maps_file_upload/public/components/json_index_file_picker.js rename to x-pack/plugins/file_upload/public/components/json_index_file_picker.js index 8721b5b60f039..a92412ae9d697 100644 --- a/x-pack/plugins/maps_file_upload/public/components/json_index_file_picker.js +++ b/x-pack/plugins/file_upload/public/components/json_index_file_picker.js @@ -10,8 +10,8 @@ import { EuiFilePicker, EuiFormRow, EuiProgress } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { parseFile } from '../util/file_parser'; -import { MAX_FILE_SIZE } from '../../common/constants/file_import'; +const MAX_FILE_SIZE = 52428800; const ACCEPTABLE_FILETYPES = ['json', 'geojson']; const acceptedFileTypeString = ACCEPTABLE_FILETYPES.map((type) => `.${type}`).join(','); const acceptedFileTypeStringMessage = ACCEPTABLE_FILETYPES.map((type) => `.${type}`).join(', '); diff --git a/x-pack/plugins/maps_file_upload/public/components/json_upload_and_parse.js b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/components/json_upload_and_parse.js rename to x-pack/plugins/file_upload/public/components/json_upload_and_parse.js diff --git a/x-pack/plugins/maps_file_upload/public/get_file_upload_component.ts b/x-pack/plugins/file_upload/public/get_file_upload_component.ts similarity index 100% rename from x-pack/plugins/maps_file_upload/public/get_file_upload_component.ts rename to x-pack/plugins/file_upload/public/get_file_upload_component.ts diff --git a/x-pack/plugins/maps_file_upload/public/index.ts b/x-pack/plugins/file_upload/public/index.ts similarity index 94% rename from x-pack/plugins/maps_file_upload/public/index.ts rename to x-pack/plugins/file_upload/public/index.ts index 95553685cbbdd..efabc984e0220 100644 --- a/x-pack/plugins/maps_file_upload/public/index.ts +++ b/x-pack/plugins/file_upload/public/index.ts @@ -11,5 +11,7 @@ export function plugin() { return new FileUploadPlugin(); } +export * from '../common'; + export { StartContract } from './plugin'; export { FileUploadComponentProps } from './get_file_upload_component'; diff --git a/x-pack/plugins/maps_file_upload/public/kibana_services.js b/x-pack/plugins/file_upload/public/kibana_services.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/kibana_services.js rename to x-pack/plugins/file_upload/public/kibana_services.js diff --git a/x-pack/plugins/maps_file_upload/public/plugin.ts b/x-pack/plugins/file_upload/public/plugin.ts similarity index 100% rename from x-pack/plugins/maps_file_upload/public/plugin.ts rename to x-pack/plugins/file_upload/public/plugin.ts diff --git a/x-pack/plugins/maps_file_upload/public/util/file_parser.js b/x-pack/plugins/file_upload/public/util/file_parser.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/file_parser.js rename to x-pack/plugins/file_upload/public/util/file_parser.js diff --git a/x-pack/plugins/maps_file_upload/public/util/file_parser.test.js b/x-pack/plugins/file_upload/public/util/file_parser.test.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/file_parser.test.js rename to x-pack/plugins/file_upload/public/util/file_parser.test.js diff --git a/x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.js b/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.js rename to x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.js diff --git a/x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.test.js b/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/geo_json_clean_and_validate.test.js rename to x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js diff --git a/x-pack/plugins/maps_file_upload/public/util/geo_processing.js b/x-pack/plugins/file_upload/public/util/geo_processing.js similarity index 76% rename from x-pack/plugins/maps_file_upload/public/util/geo_processing.js rename to x-pack/plugins/file_upload/public/util/geo_processing.js index d6f9651496aca..c90c55c2b49ac 100644 --- a/x-pack/plugins/maps_file_upload/public/util/geo_processing.js +++ b/x-pack/plugins/file_upload/public/util/geo_processing.js @@ -6,26 +6,12 @@ */ import _ from 'lodash'; -import { ES_GEO_FIELD_TYPE } from '../../common/constants/file_import'; -const DEFAULT_SETTINGS = { - number_of_shards: 1, +export const ES_GEO_FIELD_TYPE = { + GEO_POINT: 'geo_point', + GEO_SHAPE: 'geo_shape', }; -const DEFAULT_GEO_SHAPE_MAPPINGS = { - coordinates: { - type: ES_GEO_FIELD_TYPE.GEO_SHAPE, - }, -}; - -const DEFAULT_GEO_POINT_MAPPINGS = { - coordinates: { - type: ES_GEO_FIELD_TYPE.GEO_POINT, - }, -}; - -const DEFAULT_INGEST_PIPELINE = {}; - export function getGeoIndexTypesForFeatures(featureTypes) { const hasNoFeatureType = !featureTypes || !featureTypes.length; if (hasNoFeatureType) { @@ -77,11 +63,16 @@ export function geoJsonToEs(parsedGeojson, datatype) { export function getGeoJsonIndexingDetails(parsedGeojson, dataType) { return { data: geoJsonToEs(parsedGeojson, dataType), - ingestPipeline: DEFAULT_INGEST_PIPELINE, - mappings: - dataType === ES_GEO_FIELD_TYPE.GEO_POINT - ? DEFAULT_GEO_POINT_MAPPINGS - : DEFAULT_GEO_SHAPE_MAPPINGS, - settings: DEFAULT_SETTINGS, + ingestPipeline: {}, + mappings: { + properties: { + coordinates: { + type: dataType, + }, + }, + }, + settings: { + number_of_shards: 1, + }, }; } diff --git a/x-pack/plugins/maps_file_upload/public/util/geo_processing.test.js b/x-pack/plugins/file_upload/public/util/geo_processing.test.js similarity index 97% rename from x-pack/plugins/maps_file_upload/public/util/geo_processing.test.js rename to x-pack/plugins/file_upload/public/util/geo_processing.test.js index 75da5bae015af..37b665c0a3e16 100644 --- a/x-pack/plugins/maps_file_upload/public/util/geo_processing.test.js +++ b/x-pack/plugins/file_upload/public/util/geo_processing.test.js @@ -5,8 +5,7 @@ * 2.0. */ -import { geoJsonToEs } from './geo_processing'; -import { ES_GEO_FIELD_TYPE } from '../../common/constants/file_import'; +import { ES_GEO_FIELD_TYPE, geoJsonToEs } from './geo_processing'; describe('geo_processing', () => { describe('getGeoJsonToEs', () => { diff --git a/x-pack/plugins/maps_file_upload/public/util/http_service.js b/x-pack/plugins/file_upload/public/util/http_service.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/http_service.js rename to x-pack/plugins/file_upload/public/util/http_service.js diff --git a/x-pack/plugins/maps_file_upload/public/util/indexing_service.js b/x-pack/plugins/file_upload/public/util/indexing_service.js similarity index 96% rename from x-pack/plugins/maps_file_upload/public/util/indexing_service.js rename to x-pack/plugins/file_upload/public/util/indexing_service.js index c29e9685162bc..253681dad6a7d 100644 --- a/x-pack/plugins/maps_file_upload/public/util/indexing_service.js +++ b/x-pack/plugins/file_upload/public/util/indexing_service.js @@ -11,8 +11,6 @@ import { getGeoJsonIndexingDetails } from './geo_processing'; import { sizeLimitedChunking } from './size_limited_chunking'; import { i18n } from '@kbn/i18n'; -const fileType = 'json'; - export async function indexData(parsedFile, transformDetails, indexName, dataType, appName) { if (!parsedFile) { throw i18n.translate('xpack.fileUpload.indexingService.noFileImported', { @@ -117,10 +115,10 @@ function transformDataByFormatForIndexing(transform, parsedFile, dataType) { async function writeToIndex(indexingDetails) { const query = indexingDetails.id ? { id: indexingDetails.id } : null; - const { appName, index, data, settings, mappings, ingestPipeline } = indexingDetails; + const { index, data, settings, mappings, ingestPipeline } = indexingDetails; return await httpService({ - url: `/api/maps/fileupload/import`, + url: `/api/file_upload/import`, method: 'POST', ...(query ? { query } : {}), data: { @@ -129,8 +127,6 @@ async function writeToIndex(indexingDetails) { settings, mappings, ingestPipeline, - fileType, - ...(appName ? { app: appName } : {}), }, }); } diff --git a/x-pack/plugins/maps_file_upload/public/util/indexing_service.test.js b/x-pack/plugins/file_upload/public/util/indexing_service.test.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/indexing_service.test.js rename to x-pack/plugins/file_upload/public/util/indexing_service.test.js diff --git a/x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.js b/x-pack/plugins/file_upload/public/util/size_limited_chunking.js similarity index 95% rename from x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.js rename to x-pack/plugins/file_upload/public/util/size_limited_chunking.js index e42e11d0f27f0..09d4e8ca8e3a2 100644 --- a/x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.js +++ b/x-pack/plugins/file_upload/public/util/size_limited_chunking.js @@ -5,7 +5,7 @@ * 2.0. */ -import { MAX_BYTES } from '../../common/constants/file_import'; +const MAX_BYTES = 31457280; // MAX_BYTES is a good guideline for splitting up posts, but this logic // occasionally sizes chunks so closely to the limit, that the remaining content diff --git a/x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.test.js b/x-pack/plugins/file_upload/public/util/size_limited_chunking.test.js similarity index 100% rename from x-pack/plugins/maps_file_upload/public/util/size_limited_chunking.test.js rename to x-pack/plugins/file_upload/public/util/size_limited_chunking.test.js diff --git a/x-pack/plugins/file_upload/tsconfig.json b/x-pack/plugins/file_upload/tsconfig.json index f985a4599d5fe..bebb08e6dd5e3 100644 --- a/x-pack/plugins/file_upload/tsconfig.json +++ b/x-pack/plugins/file_upload/tsconfig.json @@ -10,6 +10,7 @@ "include": ["common/**/*", "public/**/*", "server/**/*"], "references": [ { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" } ] } diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 3966af9e28742..1d4f76db79751 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -11,7 +11,7 @@ "features", "inspector", "data", - "mapsFileUpload", + "fileUpload", "uiActions", "navigation", "visualizations", diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index 71be12c4304a6..44a22f1529f18 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -19,7 +19,7 @@ import { GeoJsonFileSource } from '../../sources/geojson_file_source'; import { VectorLayer } from '../../layers/vector_layer'; import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; -import { FileUploadComponentProps } from '../../../../../maps_file_upload/public'; +import { FileUploadComponentProps } from '../../../../../file_upload/public'; export const INDEX_SETUP_STEP_ID = 'INDEX_SETUP_STEP_ID'; export const INDEXING_STEP_ID = 'INDEXING_STEP_ID'; diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 4a7bccb31380d..1fbca669b0d8e 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -27,7 +27,7 @@ export const getIndexPatternService = () => pluginsStart.data.indexPatterns; export const getAutocompleteService = () => pluginsStart.data.autocomplete; export const getInspector = () => pluginsStart.inspector; export const getFileUploadComponent = async () => { - return await pluginsStart.mapsFileUpload.getFileUploadComponent(); + return await pluginsStart.fileUpload.getFileUploadComponent(); }; export const getUiSettings = () => coreStart.uiSettings; export const getIsDarkMode = () => getUiSettings().get('theme:darkMode', false); diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 4c668e0a2276b..12cff9edf55ff 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -54,7 +54,7 @@ import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; -import { StartContract as FileUploadStartContract } from '../../maps_file_upload/public'; +import { StartContract as FileUploadStartContract } from '../../file_upload/public'; import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { @@ -80,7 +80,7 @@ export interface MapsPluginStartDependencies { charts: ChartsPluginStart; data: DataPublicPluginStart; embeddable: EmbeddableStart; - mapsFileUpload: FileUploadStartContract; + fileUpload: FileUploadStartContract; inspector: InspectorStartContract; licensing: LicensingPluginStart; navigation: NavigationPublicPluginStart; diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index b70459c690c07..4a8bfe2ebae66 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -19,7 +19,7 @@ { "path": "../../../src/plugins/maps_legacy/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, - { "path": "../maps_file_upload/tsconfig.json" }, + { "path": "../file_upload/tsconfig.json" }, { "path": "../saved_objects_tagging/tsconfig.json" }, ] } diff --git a/x-pack/plugins/maps_file_upload/README.md b/x-pack/plugins/maps_file_upload/README.md deleted file mode 100644 index 1e3343664afb8..0000000000000 --- a/x-pack/plugins/maps_file_upload/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Maps File upload - -Deprecated - plugin targeted for removal and will get merged into file_upload plugin diff --git a/x-pack/plugins/maps_file_upload/common/constants/file_import.ts b/x-pack/plugins/maps_file_upload/common/constants/file_import.ts deleted file mode 100644 index 9e4763c2c8113..0000000000000 --- a/x-pack/plugins/maps_file_upload/common/constants/file_import.ts +++ /dev/null @@ -1,21 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const MAX_BYTES = 31457280; - -export const MAX_FILE_SIZE = 52428800; - -// Value to use in the Elasticsearch index mapping metadata to identify the -// index as having been created by the File Upload Plugin. -export const INDEX_META_DATA_CREATED_BY = 'file-upload-plugin'; - -export const ES_GEO_FIELD_TYPE = { - GEO_POINT: 'geo_point', - GEO_SHAPE: 'geo_shape', -}; - -export const DEFAULT_KBN_VERSION = 'kbnVersion'; diff --git a/x-pack/plugins/maps_file_upload/jest.config.js b/x-pack/plugins/maps_file_upload/jest.config.js deleted file mode 100644 index e7b45a559df10..0000000000000 --- a/x-pack/plugins/maps_file_upload/jest.config.js +++ /dev/null @@ -1,12 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/x-pack/plugins/maps_file_upload'], -}; diff --git a/x-pack/plugins/maps_file_upload/kibana.json b/x-pack/plugins/maps_file_upload/kibana.json deleted file mode 100644 index f544c56cba517..0000000000000 --- a/x-pack/plugins/maps_file_upload/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "mapsFileUpload", - "version": "8.0.0", - "kibanaVersion": "kibana", - "server": true, - "ui": true, - "requiredPlugins": ["data", "usageCollection"] -} diff --git a/x-pack/plugins/maps_file_upload/mappings.ts b/x-pack/plugins/maps_file_upload/mappings.ts deleted file mode 100644 index b8b263409f814..0000000000000 --- a/x-pack/plugins/maps_file_upload/mappings.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const mappings = { - 'file-upload-telemetry': { - properties: { - filesUploadedTotalCount: { - type: 'long', - }, - }, - }, -}; diff --git a/x-pack/plugins/maps_file_upload/server/client/errors.js b/x-pack/plugins/maps_file_upload/server/client/errors.js deleted file mode 100644 index 8f8516158b303..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/client/errors.js +++ /dev/null @@ -1,12 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { boomify } from '@hapi/boom'; - -export function wrapError(error) { - return boomify(error, { statusCode: error.status }); -} diff --git a/x-pack/plugins/maps_file_upload/server/index.js b/x-pack/plugins/maps_file_upload/server/index.js deleted file mode 100644 index 4bf4e931c7eaa..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/index.js +++ /dev/null @@ -1,12 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FileUploadPlugin } from './plugin'; - -export * from './plugin'; - -export const plugin = () => new FileUploadPlugin(); diff --git a/x-pack/plugins/maps_file_upload/server/kibana_server_services.js b/x-pack/plugins/maps_file_upload/server/kibana_server_services.js deleted file mode 100644 index 8a1278f433ab9..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/kibana_server_services.js +++ /dev/null @@ -1,12 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -let internalRepository; -export const setInternalRepository = (createInternalRepository) => { - internalRepository = createInternalRepository(); -}; -export const getInternalRepository = () => internalRepository; diff --git a/x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js b/x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js deleted file mode 100644 index 7ba491a8ea49e..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js +++ /dev/null @@ -1,161 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_import'; -import uuid from 'uuid'; - -export function importDataProvider(callWithRequest) { - async function importData(id, index, settings, mappings, ingestPipeline, data) { - let createdIndex; - let createdPipelineId; - const docCount = data.length; - - try { - const { id: pipelineId, pipeline } = ingestPipeline; - - if (!id) { - // first chunk of data, create the index and id to return - id = uuid.v1(); - - await createIndex(index, settings, mappings); - createdIndex = index; - - // create the pipeline if one has been supplied - if (pipelineId !== undefined) { - const success = await createPipeline(pipelineId, pipeline); - if (success.acknowledged !== true) { - throw success; - } - } - createdPipelineId = pipelineId; - } else { - createdIndex = index; - createdPipelineId = pipelineId; - } - - let failures = []; - if (data.length) { - const resp = await indexData(index, createdPipelineId, data); - if (resp.success === false) { - if (resp.ingestError) { - // all docs failed, abort - throw resp; - } else { - // some docs failed. - // still report success but with a list of failures - failures = resp.failures || []; - } - } - } - - return { - success: true, - id, - index: createdIndex, - pipelineId: createdPipelineId, - docCount, - failures, - }; - } catch (error) { - return { - success: false, - id, - index: createdIndex, - pipelineId: createdPipelineId, - error: error.error !== undefined ? error.error : error, - docCount, - ingestError: error.ingestError, - failures: error.failures || [], - }; - } - } - - async function createIndex(index, settings, mappings) { - const body = { - mappings: { - _meta: { - created_by: INDEX_META_DATA_CREATED_BY, - }, - properties: mappings, - }, - }; - - if (settings && Object.keys(settings).length) { - body.settings = settings; - } - - await callWithRequest('indices.create', { index, body }); - } - - async function indexData(index, pipelineId, data) { - try { - const body = []; - for (let i = 0; i < data.length; i++) { - body.push({ index: {} }); - body.push(data[i]); - } - - const settings = { index, body }; - if (pipelineId !== undefined) { - settings.pipeline = pipelineId; - } - - const resp = await callWithRequest('bulk', settings); - if (resp.errors) { - throw resp; - } else { - return { - success: true, - docs: data.length, - failures: [], - }; - } - } catch (error) { - let failures = []; - let ingestError = false; - if (error.errors !== undefined && Array.isArray(error.items)) { - // an expected error where some or all of the bulk request - // docs have failed to be ingested. - failures = getFailures(error.items, data); - } else { - // some other error has happened. - ingestError = true; - } - - return { - success: false, - error, - docCount: data.length, - failures, - ingestError, - }; - } - } - - async function createPipeline(id, pipeline) { - return await callWithRequest('ingest.putPipeline', { id, body: pipeline }); - } - - function getFailures(items, data) { - const failures = []; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (item.index && item.index.error) { - failures.push({ - item: i, - reason: item.index.error.reason, - doc: data[i], - }); - } - } - return failures; - } - - return { - importData, - }; -} diff --git a/x-pack/plugins/maps_file_upload/server/models/import_data/index.js b/x-pack/plugins/maps_file_upload/server/models/import_data/index.js deleted file mode 100644 index c1ba4b84975e5..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/models/import_data/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { importDataProvider } from './import_data'; diff --git a/x-pack/plugins/maps_file_upload/server/plugin.js b/x-pack/plugins/maps_file_upload/server/plugin.js deleted file mode 100644 index 1072da863acc7..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/plugin.js +++ /dev/null @@ -1,27 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { initRoutes } from './routes/file_upload'; -import { setInternalRepository } from './kibana_server_services'; -import { registerFileUploadUsageCollector, fileUploadTelemetryMappingsType } from './telemetry'; - -export class FileUploadPlugin { - constructor() { - this.router = null; - } - - setup(core, plugins) { - core.savedObjects.registerType(fileUploadTelemetryMappingsType); - this.router = core.http.createRouter(); - registerFileUploadUsageCollector(plugins.usageCollection); - } - - start(core) { - initRoutes(this.router, core.savedObjects.getSavedObjectsRepository); - setInternalRepository(core.savedObjects.createInternalRepository); - } -} diff --git a/x-pack/plugins/maps_file_upload/server/routes/file_upload.js b/x-pack/plugins/maps_file_upload/server/routes/file_upload.js deleted file mode 100644 index 517fab13363a2..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/routes/file_upload.js +++ /dev/null @@ -1,131 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { importDataProvider } from '../models/import_data'; -import { updateTelemetry } from '../telemetry/telemetry'; -import { MAX_BYTES } from '../../common/constants/file_import'; -import { schema } from '@kbn/config-schema'; - -export const IMPORT_ROUTE = '/api/maps/fileupload/import'; - -export const querySchema = schema.maybe( - schema.object({ - id: schema.nullable(schema.string()), - }) -); - -export const bodySchema = schema.object( - { - app: schema.maybe(schema.string()), - index: schema.string(), - fileType: schema.string(), - ingestPipeline: schema.maybe( - schema.object( - {}, - { - defaultValue: {}, - unknowns: 'allow', - } - ) - ), - }, - { unknowns: 'allow' } -); - -const options = { - body: { - maxBytes: MAX_BYTES, - accepts: ['application/json'], - }, - tags: ['access:fileUpload:import'], -}; - -export const idConditionalValidation = (body, boolHasId) => - schema - .object( - { - data: boolHasId - ? schema.arrayOf(schema.object({}, { unknowns: 'allow' }), { minSize: 1 }) - : schema.any(), - settings: boolHasId - ? schema.any() - : schema.object( - {}, - { - defaultValue: { - number_of_shards: 1, - }, - unknowns: 'allow', - } - ), - mappings: boolHasId - ? schema.any() - : schema.object( - {}, - { - defaultValue: {}, - unknowns: 'allow', - } - ), - }, - { unknowns: 'allow' } - ) - .validate(body); - -const finishValidationAndProcessReq = () => { - return async (con, req, { ok, badRequest }) => { - const { - query: { id }, - body, - } = req; - const boolHasId = !!id; - - let resp; - try { - const validIdReqData = idConditionalValidation(body, boolHasId); - const callWithRequest = con.core.elasticsearch.legacy.client.callAsCurrentUser; - const { importData: importDataFunc } = importDataProvider(callWithRequest); - - const { index, settings, mappings, ingestPipeline, data } = validIdReqData; - const processedReq = await importDataFunc( - id, - index, - settings, - mappings, - ingestPipeline, - data - ); - - if (processedReq.success) { - resp = ok({ body: processedReq }); - // If no id's been established then this is a new index, update telemetry - if (!boolHasId) { - await updateTelemetry(); - } - } else { - resp = badRequest(`Error processing request 1: ${processedReq.error.message}`, ['body']); - } - } catch (e) { - resp = badRequest(`Error processing request 2: : ${e.message}`, ['body']); - } - return resp; - }; -}; - -export const initRoutes = (router) => { - router.post( - { - path: `${IMPORT_ROUTE}{id?}`, - validate: { - query: querySchema, - body: bodySchema, - }, - options, - }, - finishValidationAndProcessReq() - ); -}; diff --git a/x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js b/x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js deleted file mode 100644 index e893e103aad72..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js +++ /dev/null @@ -1,77 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { querySchema, bodySchema, idConditionalValidation } from './file_upload'; - -const queryWithId = { - id: '123', -}; - -const bodyWithoutQueryId = { - index: 'islandofone', - data: [], - settings: { number_of_shards: 1 }, - mappings: { coordinates: { type: 'geo_point' } }, - ingestPipeline: {}, - fileType: 'json', - app: 'Maps', -}; - -const bodyWithQueryId = { - index: 'islandofone2', - data: [{ coordinates: [], name: 'islandofone2' }], - settings: {}, - mappings: {}, - ingestPipeline: {}, - fileType: 'json', -}; - -describe('route validation', () => { - it(`validates query with id`, async () => { - const validationResult = querySchema.validate(queryWithId); - expect(validationResult.id).toBe(queryWithId.id); - }); - - it(`validates query without id`, async () => { - const validationResult = querySchema.validate({}); - expect(validationResult.id).toBeNull(); - }); - - it(`throws when query contains content other than an id`, async () => { - expect(() => querySchema.validate({ notAnId: 123 })).toThrowError( - `[notAnId]: definition for this key is missing` - ); - }); - - it(`validates body with valid fields`, async () => { - const validationResult = bodySchema.validate(bodyWithoutQueryId); - expect(validationResult).toEqual(bodyWithoutQueryId); - }); - - it(`throws if an expected field is missing`, async () => { - /* eslint-disable no-unused-vars */ - const { index, ...bodyWithoutIndexField } = bodyWithoutQueryId; - expect(() => bodySchema.validate(bodyWithoutIndexField)).toThrowError( - `[index]: expected value of type [string] but got [undefined]` - ); - }); - - it(`validates conditional fields when id has been provided in query`, async () => { - const validationResult = idConditionalValidation(bodyWithQueryId, true); - expect(validationResult).toEqual(bodyWithQueryId); - }); - - it(`validates conditional fields when no id has been provided in query`, async () => { - const validationResultWhenIdPresent = idConditionalValidation(bodyWithoutQueryId, false); - expect(validationResultWhenIdPresent).toEqual(bodyWithoutQueryId); - // Conditions for no id are more strict since this query sets up the index, - // expect it to throw if expected fields aren't present - expect(() => idConditionalValidation(bodyWithoutQueryId, true)).toThrowError( - `[data]: array size is [0], but cannot be smaller than [1]` - ); - }); -}); diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts b/x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts deleted file mode 100644 index bf786aa830448..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts +++ /dev/null @@ -1,29 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { getTelemetry, initTelemetry, Telemetry } from './telemetry'; - -export function registerFileUploadUsageCollector(usageCollection: UsageCollectionSetup): void { - const fileUploadUsageCollector = usageCollection.makeUsageCollector({ - type: 'fileUploadTelemetry', - isReady: () => true, - fetch: async () => { - const fileUploadUsage = await getTelemetry(); - if (!fileUploadUsage) { - return initTelemetry(); - } - - return fileUploadUsage; - }, - schema: { - filesUploadedTotalCount: { type: 'long' }, - }, - }); - - usageCollection.registerCollector(fileUploadUsageCollector); -} diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/index.ts b/x-pack/plugins/maps_file_upload/server/telemetry/index.ts deleted file mode 100644 index 83cd64c3f0e6f..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/telemetry/index.ts +++ /dev/null @@ -1,9 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { registerFileUploadUsageCollector } from './file_upload_usage_collector'; -export { fileUploadTelemetryMappingsType } from './mappings'; diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts b/x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts deleted file mode 100644 index ee79e2f6c6d47..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts +++ /dev/null @@ -1,22 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectsType } from 'src/core/server'; -import { TELEMETRY_DOC_ID } from './telemetry'; - -export const fileUploadTelemetryMappingsType: SavedObjectsType = { - name: TELEMETRY_DOC_ID, - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - filesUploadedTotalCount: { - type: 'long', - }, - }, - }, -}; diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts b/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts deleted file mode 100644 index 2ca01b03aa633..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts +++ /dev/null @@ -1,48 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getTelemetry, updateTelemetry } from './telemetry'; - -const internalRepository = () => ({ - get: jest.fn(() => null), - create: jest.fn(() => ({ attributes: 'test' })), - update: jest.fn(() => ({ attributes: 'test' })), -}); - -function mockInit(getVal: any = { attributes: {} }): any { - return { - ...internalRepository(), - get: jest.fn(() => getVal), - }; -} - -describe('file upload plugin telemetry', () => { - describe('getTelemetry', () => { - it('should get existing telemetry', async () => { - const internalRepo = mockInit(); - await getTelemetry(internalRepo); - expect(internalRepo.update.mock.calls.length).toBe(0); - expect(internalRepo.get.mock.calls.length).toBe(1); - expect(internalRepo.create.mock.calls.length).toBe(0); - }); - }); - - describe('updateTelemetry', () => { - it('should update existing telemetry', async () => { - const internalRepo = mockInit({ - attributes: { - filesUploadedTotalCount: 2, - }, - }); - - await updateTelemetry(internalRepo); - expect(internalRepo.update.mock.calls.length).toBe(1); - expect(internalRepo.get.mock.calls.length).toBe(1); - expect(internalRepo.create.mock.calls.length).toBe(0); - }); - }); -}); diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts b/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts deleted file mode 100644 index 0e53c2570e01b..0000000000000 --- a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts +++ /dev/null @@ -1,62 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import _ from 'lodash'; -// @ts-ignore -import { getInternalRepository } from '../kibana_server_services'; - -export const TELEMETRY_DOC_ID = 'file-upload-telemetry'; - -export interface Telemetry { - filesUploadedTotalCount: number; -} - -export interface TelemetrySavedObject { - attributes: Telemetry; -} - -export function initTelemetry(): Telemetry { - return { - filesUploadedTotalCount: 0, - }; -} - -export async function getTelemetry(internalRepo?: object): Promise { - const internalRepository = internalRepo || getInternalRepository(); - let telemetrySavedObject; - - try { - telemetrySavedObject = await internalRepository.get(TELEMETRY_DOC_ID, TELEMETRY_DOC_ID); - } catch (e) { - // Fail silently - } - - return telemetrySavedObject ? telemetrySavedObject.attributes : null; -} - -export async function updateTelemetry(internalRepo?: any) { - const internalRepository = internalRepo || getInternalRepository(); - let telemetry = await getTelemetry(internalRepository); - // Create if doesn't exist - if (!telemetry || _.isEmpty(telemetry)) { - const newTelemetrySavedObject = await internalRepository.create( - TELEMETRY_DOC_ID, - initTelemetry(), - { id: TELEMETRY_DOC_ID } - ); - telemetry = newTelemetrySavedObject.attributes; - } - - await internalRepository.update(TELEMETRY_DOC_ID, TELEMETRY_DOC_ID, incrementCounts(telemetry)); -} - -export function incrementCounts({ filesUploadedTotalCount }: { filesUploadedTotalCount: number }) { - return { - // TODO: get telemetry for app, total file counts, file type - filesUploadedTotalCount: filesUploadedTotalCount + 1, - }; -} diff --git a/x-pack/plugins/maps_file_upload/tsconfig.json b/x-pack/plugins/maps_file_upload/tsconfig.json deleted file mode 100644 index f068d62b71739..0000000000000 --- a/x-pack/plugins/maps_file_upload/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["common/**/*", "public/**/*", "server/**/*", "mappings.ts"], - "references": [ - { "path": "../../../src/plugins/data/tsconfig.json" }, - { "path": "../../../src/plugins/usage_collection/tsconfig.json" } - ] -} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx index 0535b15912a9b..0fa7de4732c39 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx @@ -12,7 +12,7 @@ import { EuiCallOut, EuiSpacer, EuiButtonEmpty, EuiHorizontalRule } from '@elast import numeral from '@elastic/numeral'; import { ErrorResponse } from '../../../../../../common/types/errors'; -import { FILE_SIZE_DISPLAY_FORMAT } from '../../../../../../../file_upload/common'; +import { FILE_SIZE_DISPLAY_FORMAT } from '../../../../../../../file_upload/public'; interface FileTooLargeProps { fileSize: number; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 47f262ef45a18..4412390d62c1f 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts @@ -15,7 +15,7 @@ import { MAX_FILE_SIZE_BYTES, ABSOLUTE_MAX_FILE_SIZE_BYTES, FILE_SIZE_DISPLAY_FORMAT, -} from '../../../../../../../file_upload/common'; +} from '../../../../../../../file_upload/public'; import { getUiSettings } from '../../../../util/dependency_cache'; import { FILE_DATA_VISUALIZER_MAX_FILE_SIZE } from '../../../../../../common/constants/settings'; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 9e6a0c06808bc..d4b07203e8109 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -2219,13 +2219,6 @@ } } }, - "fileUploadTelemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, "maps": { "properties": { "settings": { diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js index 9a6602ef923d3..3be24b273ae4c 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js @@ -83,7 +83,7 @@ export default function ({ getService }) { expect(stats.stack_stats.kibana.plugins.reporting.enabled).to.be(true); expect(stats.stack_stats.kibana.plugins.rollups.index_patterns).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.spaces.available).to.be(true); - expect(stats.stack_stats.kibana.plugins.fileUploadTelemetry.filesUploadedTotalCount).to.be.a( + expect(stats.stack_stats.kibana.plugins.fileUpload.file_upload.index_creation_count).to.be.a( 'number' ); diff --git a/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js b/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js index 4496b59393eec..0ce9b7022b38d 100644 --- a/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js +++ b/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js @@ -11,7 +11,6 @@ import uuid from 'uuid/v4'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['maps', 'common']); - const testSubjects = getService('testSubjects'); const log = getService('log'); const security = getService('security'); @@ -99,20 +98,6 @@ export default function ({ getService, getPageObjects }) { expect(newIndexedLayerExists).to.be(false); }); - it('should create a link to new index in management', async () => { - const indexName = await indexPoint(); - - const layerAddReady = await PageObjects.maps.importLayerReadyForAdd(); - expect(layerAddReady).to.be(true); - - const newIndexLinkExists = await testSubjects.exists('indexManagementNewIndexLink'); - expect(newIndexLinkExists).to.be(true); - - const indexLink = await testSubjects.getAttribute('indexManagementNewIndexLink', 'href'); - const linkDirectsToNewIndex = indexLink.endsWith(indexName); - expect(linkDirectsToNewIndex).to.be(true); - }); - const GEO_POINT = 'geo_point'; const pointGeojsonFiles = ['point.json', 'multi_point.json']; pointGeojsonFiles.forEach(async (pointFile) => { diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index f4497487f6ff9..00286ac47da6e 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -27,7 +27,6 @@ "plugins/licensing/**/*", "plugins/lens/**/*", "plugins/maps/**/*", - "plugins/maps_file_upload/**/*", "plugins/maps_legacy_licensing/**/*", "plugins/ml/**/*", "plugins/observability/**/*", @@ -130,7 +129,6 @@ { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, - { "path": "./plugins/maps_file_upload/tsconfig.json" }, { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./plugins/maps/tsconfig.json" }, { "path": "./plugins/ml/tsconfig.json" }, diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index d4d7e8caa5088..a36f4e205ab7d 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -29,7 +29,6 @@ { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, - { "path": "./plugins/maps_file_upload/tsconfig.json" }, { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./plugins/maps/tsconfig.json" }, { "path": "./plugins/ml/tsconfig.json" }, From 646732f96d4842bc1b66854f4a94c87bfc3959c0 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 9 Feb 2021 17:49:53 -0700 Subject: [PATCH 090/100] [core.logging] Uses host timezone as default (#90368) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/migration/migrate_8_0.asciidoc | 13 +++++++- docs/setup/settings.asciidoc | 3 +- .../deprecation/core_deprecations.test.ts | 20 ++++++++++++ .../config/deprecation/core_deprecations.ts | 13 ++++++++ .../legacy/integration_tests/logging.test.ts | 20 ++++++------ src/core/server/logging/README.md | 31 ++++++++++--------- .../__snapshots__/logging_system.test.ts.snap | 14 ++++----- .../__snapshots__/pattern_layout.test.ts.snap | 24 +++++++------- .../logging/layouts/conversions/date.ts | 11 ++++--- .../logging/layouts/pattern_layout.test.ts | 12 ++++--- 10 files changed, 106 insertions(+), 55 deletions(-) diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 14eff4594c813..5452621440ed8 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -52,7 +52,18 @@ for example, `logstash-*`. ==== Default logging timezone is now the system's timezone *Details:* In prior releases the timezone used in logs defaulted to UTC. We now use the host machine's timezone by default. -*Impact:* To restore the previous behavior, in kibana.yml set `logging.timezone: UTC`. +*Impact:* To restore the previous behavior, in kibana.yml use the pattern layout, with a date modifier: +[source,yaml] +------------------- +logging: + appenders: + console: + kind: console + layout: + kind: pattern + pattern: "%date{ISO8601_TZ}{UTC}" +------------------- +See https://github.com/elastic/kibana/pull/90368 for more details. [float] ==== Responses are never logged by default diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index ecdb41c897b12..9b9c26fd0e1db 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -309,7 +309,8 @@ suppress all logging output. *Default: `false`* | Set to the canonical time zone ID (for example, `America/Los_Angeles`) to log events using that time zone. For possible values, refer to -https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[database time zones]. *Default: `UTC`* +https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[database time zones]. +When not set, log events use the host timezone | [[logging-verbose]] `logging.verbose:` {ess-icon} | Set to `true` to log all events, including system usage information and all diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index 70ca91b0d6317..4d7dafd2162c2 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -298,4 +298,24 @@ describe('core deprecations', () => { expect(messages).toEqual([]); }); }); + + describe('logging.timezone', () => { + it('warns when ops events are used', () => { + const { messages } = applyCoreDeprecations({ + logging: { timezone: 'GMT' }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"logging.timezone\\" has been deprecated and will be removed in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md", + ] + `); + }); + + it('does not warn when other events are configured', () => { + const { messages } = applyCoreDeprecations({ + logging: { events: { log: '*' } }, + }); + expect(messages).toEqual([]); + }); + }); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 0db53cdb2e8be..fbdbaeb14fd59 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -127,6 +127,18 @@ const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, l return settings; }; +const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'logging.timezone')) { + log( + '"logging.timezone" has been deprecated and will be removed ' + + 'in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern ' + + 'in your logging configuration. For more details, see ' + + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.md' + ); + } + return settings; +}; + export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [ unusedFromRoot('savedObjects.indexCheckTimeout'), unusedFromRoot('server.xsrf.token'), @@ -163,4 +175,5 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unu mapManifestServiceUrlDeprecation, opsLoggingEventDeprecation, requestLoggingEventDeprecation, + timezoneLoggingDeprecation, ]; diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts index 6588f4270fe18..321eb81708f1e 100644 --- a/src/core/server/legacy/integration_tests/logging.test.ts +++ b/src/core/server/legacy/integration_tests/logging.test.ts @@ -87,7 +87,7 @@ describe('logging service', () => { const loggedString = getPlatformLogsFromMock(mockConsoleLog); expect(loggedString).toMatchInlineSnapshot(` Array [ - "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] handled by NP", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] handled by NP", ] `); }); @@ -131,9 +131,9 @@ describe('logging service', () => { expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` Array [ - "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error", ] `); @@ -162,9 +162,9 @@ describe('logging service', () => { expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` Array [ - "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error", ] `); @@ -199,9 +199,9 @@ describe('logging service', () => { expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` Array [ - "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", - "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error", ] `); diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index b0759defb8803..9e3da1f3e0d71 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -110,7 +110,8 @@ Example of `%meta` output: ##### date Outputs the date of the logging event. The date conversion specifier may be followed by a set of braces containing a name of predefined date format and canonical timezone name. -Timezone name is expected to be one from [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) +Timezone name is expected to be one from [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). +Timezone defaults to the host timezone when not explicitly specified. Example of `%date` output: | Conversion pattern | Example | @@ -410,22 +411,22 @@ loggerWithNestedContext.debug('Message with `debug` log level.'); And assuming logger for `server` context with `console` appender and `trace` level was used, console output will look like this: ```bash -[2017-07-25T18:54:41.639Z][TRACE][server] Message with `trace` log level. -[2017-07-25T18:54:41.639Z][DEBUG][server] Message with `debug` log level. -[2017-07-25T18:54:41.639Z][INFO ][server] Message with `info` log level. -[2017-07-25T18:54:41.639Z][WARN ][server] Message with `warn` log level. -[2017-07-25T18:54:41.639Z][ERROR][server] Message with `error` log level. -[2017-07-25T18:54:41.639Z][FATAL][server] Message with `fatal` log level. - -[2017-07-25T18:54:41.639Z][TRACE][server.http] Message with `trace` log level. -[2017-07-25T18:54:41.639Z][DEBUG][server.http] Message with `debug` log level. +[2017-07-25T11:54:41.639-07:00][TRACE][server] Message with `trace` log level. +[2017-07-25T11:54:41.639-07:00][DEBUG][server] Message with `debug` log level. +[2017-07-25T11:54:41.639-07:00][INFO ][server] Message with `info` log level. +[2017-07-25T11:54:41.639-07:00][WARN ][server] Message with `warn` log level. +[2017-07-25T11:54:41.639-07:00][ERROR][server] Message with `error` log level. +[2017-07-25T11:54:41.639-07:00][FATAL][server] Message with `fatal` log level. + +[2017-07-25T11:54:41.639-07:00][TRACE][server.http] Message with `trace` log level. +[2017-07-25T11:54:41.639-07:00][DEBUG][server.http] Message with `debug` log level. ``` The log will be less verbose with `warn` level for the `server` context: ```bash -[2017-07-25T18:54:41.639Z][WARN ][server] Message with `warn` log level. -[2017-07-25T18:54:41.639Z][ERROR][server] Message with `error` log level. -[2017-07-25T18:54:41.639Z][FATAL][server] Message with `fatal` log level. +[2017-07-25T11:54:41.639-07:00][WARN ][server] Message with `warn` log level. +[2017-07-25T11:54:41.639-07:00][ERROR][server] Message with `error` log level. +[2017-07-25T11:54:41.639-07:00][FATAL][server] Message with `fatal` log level. ``` ### Logging config migration @@ -488,7 +489,7 @@ logging.root.level: all #### logging.timezone Set to the canonical timezone id to log events using that timezone. New logging config allows -to [specify timezone](#date) for `layout: pattern`. +to [specify timezone](#date) for `layout: pattern`. Defaults to host timezone when not specified. ```yaml logging: appenders: @@ -530,7 +531,7 @@ TBD | Parameter | Platform log record in **pattern** format | Legacy Platform log record **text** format | | --------------- | ------------------------------------------ | ------------------------------------------ | -| @timestamp | ISO8601 `2012-01-31T23:33:22.011Z` | Absolute `23:33:22.011` | +| @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | Absolute `23:33:22.011` | | context | `parent.child` | `['parent', 'child']` | | level | `DEBUG` | `['debug']` | | meta | stringified JSON object `{"to": "v8"}` | N/A | diff --git a/src/core/server/logging/__snapshots__/logging_system.test.ts.snap b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap index cbe0e352a0f3a..8013aec4a06fd 100644 --- a/src/core/server/logging/__snapshots__/logging_system.test.ts.snap +++ b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap @@ -1,20 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`appends records via multiple appenders.: console logs 1`] = `"[2012-01-31T23:33:22.011Z][INFO ][some-context] You know, just for your info."`; +exports[`appends records via multiple appenders.: console logs 1`] = `"[2012-01-31T18:33:22.011-05:00][INFO ][some-context] You know, just for your info."`; exports[`appends records via multiple appenders.: file logs 1`] = ` -"[2012-01-31T23:33:22.011Z][WARN ][tests] Config is not ready! +"[2012-01-31T13:33:22.011-05:00][WARN ][tests] Config is not ready! " `; exports[`appends records via multiple appenders.: file logs 2`] = ` -"[2012-01-31T23:33:22.011Z][ERROR][tests.child] Too bad that config is not ready :/ +"[2012-01-31T08:33:22.011-05:00][ERROR][tests.child] Too bad that config is not ready :/ " `; exports[`asLoggerFactory() only allows to create new loggers. 1`] = ` Object { - "@timestamp": "2012-01-31T18:33:22.011-05:00", + "@timestamp": "2012-01-30T22:33:22.011-05:00", "log": Object { "level": "TRACE", "logger": "test.context", @@ -28,7 +28,7 @@ Object { exports[`asLoggerFactory() only allows to create new loggers. 2`] = ` Object { - "@timestamp": "2012-01-31T13:33:22.011-05:00", + "@timestamp": "2012-01-30T17:33:22.011-05:00", "log": Object { "level": "INFO", "logger": "test.context", @@ -43,7 +43,7 @@ Object { exports[`asLoggerFactory() only allows to create new loggers. 3`] = ` Object { - "@timestamp": "2012-01-31T08:33:22.011-05:00", + "@timestamp": "2012-01-30T12:33:22.011-05:00", "log": Object { "level": "FATAL", "logger": "test.context", @@ -87,7 +87,7 @@ Object { exports[`uses \`root\` logger if context is not specified. 1`] = ` Array [ Array [ - "[2012-01-31T23:33:22.011Z][INFO ][root] This message goes to a root context.", + "[2012-01-31T03:33:22.011-05:00][INFO ][root] This message goes to a root context.", ], ] `; diff --git a/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap index 8988f3019d509..54e46ca7f520e 100644 --- a/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap +++ b/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap @@ -12,29 +12,29 @@ exports[`\`format()\` correctly formats record with custom pattern. 5`] = `"mock exports[`\`format()\` correctly formats record with custom pattern. 6`] = `"mock-message-6-context-6-message-6"`; -exports[`\`format()\` correctly formats record with full pattern. 1`] = `"[2012-02-01T14:30:22.011Z][FATAL][context-1] Some error stack"`; +exports[`\`format()\` correctly formats record with full pattern. 1`] = `"[2012-02-01T09:30:22.011-05:00][FATAL][context-1] Some error stack"`; -exports[`\`format()\` correctly formats record with full pattern. 2`] = `"[2012-02-01T14:30:22.011Z][ERROR][context-2] message-2"`; +exports[`\`format()\` correctly formats record with full pattern. 2`] = `"[2012-02-01T09:30:22.011-05:00][ERROR][context-2] message-2"`; -exports[`\`format()\` correctly formats record with full pattern. 3`] = `"[2012-02-01T14:30:22.011Z][WARN ][context-3] message-3"`; +exports[`\`format()\` correctly formats record with full pattern. 3`] = `"[2012-02-01T09:30:22.011-05:00][WARN ][context-3] message-3"`; -exports[`\`format()\` correctly formats record with full pattern. 4`] = `"[2012-02-01T14:30:22.011Z][DEBUG][context-4] message-4"`; +exports[`\`format()\` correctly formats record with full pattern. 4`] = `"[2012-02-01T09:30:22.011-05:00][DEBUG][context-4] message-4"`; -exports[`\`format()\` correctly formats record with full pattern. 5`] = `"[2012-02-01T14:30:22.011Z][INFO ][context-5] message-5"`; +exports[`\`format()\` correctly formats record with full pattern. 5`] = `"[2012-02-01T09:30:22.011-05:00][INFO ][context-5] message-5"`; -exports[`\`format()\` correctly formats record with full pattern. 6`] = `"[2012-02-01T14:30:22.011Z][TRACE][context-6] message-6"`; +exports[`\`format()\` correctly formats record with full pattern. 6`] = `"[2012-02-01T09:30:22.011-05:00][TRACE][context-6] message-6"`; -exports[`\`format()\` correctly formats record with highlighting. 1`] = `[2012-02-01T14:30:22.011Z][FATAL][context-1] Some error stack`; +exports[`\`format()\` correctly formats record with highlighting. 1`] = `[2012-02-01T09:30:22.011-05:00][FATAL][context-1] Some error stack`; -exports[`\`format()\` correctly formats record with highlighting. 2`] = `[2012-02-01T14:30:22.011Z][ERROR][context-2] message-2`; +exports[`\`format()\` correctly formats record with highlighting. 2`] = `[2012-02-01T09:30:22.011-05:00][ERROR][context-2] message-2`; -exports[`\`format()\` correctly formats record with highlighting. 3`] = `[2012-02-01T14:30:22.011Z][WARN ][context-3] message-3`; +exports[`\`format()\` correctly formats record with highlighting. 3`] = `[2012-02-01T09:30:22.011-05:00][WARN ][context-3] message-3`; -exports[`\`format()\` correctly formats record with highlighting. 4`] = `[2012-02-01T14:30:22.011Z][DEBUG][context-4] message-4`; +exports[`\`format()\` correctly formats record with highlighting. 4`] = `[2012-02-01T09:30:22.011-05:00][DEBUG][context-4] message-4`; -exports[`\`format()\` correctly formats record with highlighting. 5`] = `[2012-02-01T14:30:22.011Z][INFO ][context-5] message-5`; +exports[`\`format()\` correctly formats record with highlighting. 5`] = `[2012-02-01T09:30:22.011-05:00][INFO ][context-5] message-5`; -exports[`\`format()\` correctly formats record with highlighting. 6`] = `[2012-02-01T14:30:22.011Z][TRACE][context-6] message-6`; +exports[`\`format()\` correctly formats record with highlighting. 6`] = `[2012-02-01T09:30:22.011-05:00][TRACE][context-6] message-6`; exports[`allows specifying the PID in custom pattern 1`] = `"5355-context-1-Some error stack"`; diff --git a/src/core/server/logging/layouts/conversions/date.ts b/src/core/server/logging/layouts/conversions/date.ts index c1f871282c5de..66aad5b42354a 100644 --- a/src/core/server/logging/layouts/conversions/date.ts +++ b/src/core/server/logging/layouts/conversions/date.ts @@ -22,11 +22,14 @@ const formats = { UNIX_MILLIS: 'UNIX_MILLIS', }; -function formatDate(date: Date, dateFormat: string = formats.ISO8601, timezone?: string): string { +function formatDate( + date: Date, + dateFormat: string = formats.ISO8601_TZ, + timezone?: string +): string { const momentDate = moment(date); - if (timezone) { - momentDate.tz(timezone); - } + momentDate.tz(timezone ?? moment.tz.guess()); + switch (dateFormat) { case formats.ISO8601: return momentDate.toISOString(); diff --git a/src/core/server/logging/layouts/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts index d291516524be0..7dd3c7c51f833 100644 --- a/src/core/server/logging/layouts/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -122,7 +122,9 @@ test('`format()` correctly formats record with meta data.', () => { to: 'v8', }, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{"from":"v7","to":"v8"} message-meta'); + ).toBe( + '[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta]{"from":"v7","to":"v8"} message-meta' + ); expect( layout.format({ @@ -133,7 +135,7 @@ test('`format()` correctly formats record with meta data.', () => { pid: 5355, meta: {}, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{} message-meta'); + ).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta]{} message-meta'); expect( layout.format({ @@ -143,7 +145,7 @@ test('`format()` correctly formats record with meta data.', () => { timestamp, pid: 5355, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta] message-meta'); + ).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta] message-meta'); }); test('`format()` correctly formats record with highlighting.', () => { @@ -187,10 +189,10 @@ describe('format', () => { timestamp, pid: 5355, }; - it('uses ISO8601 as default', () => { + it('uses ISO8601_TZ as default', () => { const layout = new PatternLayout(); - expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context] message'); + expect(layout.format(record)).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context] message'); }); describe('supports specifying a predefined format', () => { From 59cc39434ee43f7220092f47732550cc1efc7900 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 9 Feb 2021 17:57:05 -0700 Subject: [PATCH 091/100] remove ref to removed tsconfig file --- tsconfig.refs.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 86fdfad2f524d..d5482a85856fe 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -83,7 +83,6 @@ { "path": "./x-pack/plugins/lens/tsconfig.json" }, { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_file_upload/tsconfig.json" }, { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, From 38a647539643700d66179532bec78d62ffd8676c Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Tue, 9 Feb 2021 20:30:25 -0600 Subject: [PATCH 092/100] Revert "Revert "[Metrics UI] Add Metrics Anomaly Alert Type (#89244)"" (#90889) * Revert "Revert "[Metrics UI] Add Metrics Anomaly Alert Type (#89244)"" This reverts commit 8166becc5555f132636bc1e8662370d1b4bf7b6a. * Fix type error --- .../infra/common/alerting/metrics/types.ts | 43 ++- .../infra/common/infra_ml/anomaly_results.ts | 56 +-- .../common/components/alert_preview.tsx | 5 +- .../common/components/get_alert_preview.ts | 4 +- .../components/metrics_alert_dropdown.tsx | 151 +++++++++ .../inventory/components/alert_dropdown.tsx | 59 ---- .../inventory/components/alert_flyout.tsx | 18 +- .../inventory/components/node_type.tsx | 2 +- .../components/alert_flyout.tsx | 53 +++ .../components/expression.test.tsx | 74 ++++ .../metric_anomaly/components/expression.tsx | 320 ++++++++++++++++++ .../components/influencer_filter.tsx | 193 +++++++++++ .../metric_anomaly/components/node_type.tsx | 117 +++++++ .../components/severity_threshold.tsx | 140 ++++++++ .../metric_anomaly/components/validation.tsx | 35 ++ .../public/alerting/metric_anomaly/index.ts | 46 +++ .../components/alert_dropdown.tsx | 57 ---- .../components/alert_flyout.tsx | 11 +- .../logging/log_analysis_setup/index.ts | 1 - .../subscription_splash_content.tsx | 176 ---------- .../source_configuration_settings.tsx | 4 +- .../subscription_splash_content.tsx | 110 +++--- .../containers/ml/infra_ml_capabilities.tsx | 4 +- .../containers/with_kuery_autocompletion.tsx | 11 +- .../log_entry_categories/page_content.tsx | 2 +- .../logs/log_entry_rate/page_content.tsx | 2 +- .../infra/public/pages/metrics/index.tsx | 8 +- ...lyout.tsx => anomaly_detection_flyout.tsx} | 0 .../ml/anomaly_detection/flyout_home.tsx | 6 +- .../metrics_explorer/components/kuery_bar.tsx | 21 +- x-pack/plugins/infra/public/plugin.ts | 2 + x-pack/plugins/infra/public/types.ts | 3 +- .../metric_anomaly/evaluate_condition.ts | 51 +++ .../metric_anomaly/metric_anomaly_executor.ts | 142 ++++++++ .../preview_metric_anomaly_alert.ts | 120 +++++++ .../register_metric_anomaly_alert_type.ts | 110 ++++++ .../lib/alerting/register_alert_types.ts | 10 +- .../infra/server/lib/infra_ml/common.ts | 17 + .../infra/server/lib/infra_ml/index.ts | 1 + .../lib/infra_ml/metrics_hosts_anomalies.ts | 39 +-- .../lib/infra_ml/metrics_k8s_anomalies.ts | 39 +-- .../server/lib/infra_ml/queries/common.ts | 32 ++ .../queries/metrics_hosts_anomalies.ts | 12 +- .../infra_ml/queries/metrics_k8s_anomalies.ts | 12 +- x-pack/plugins/infra/server/plugin.ts | 2 +- .../infra/server/routes/alerting/preview.ts | 51 ++- .../results/metrics_hosts_anomalies.ts | 2 +- .../infra_ml/results/metrics_k8s_anomalies.ts | 2 +- .../translations/translations/ja-JP.json | 11 - .../translations/translations/zh-CN.json | 11 - 50 files changed, 1918 insertions(+), 480 deletions(-) create mode 100644 x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts delete mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx delete mode 100644 x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx rename x-pack/plugins/infra/public/{pages/metrics/inventory_view/components/ml/anomaly_detection => components}/subscription_splash_content.tsx (58%) rename x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/{anomoly_detection_flyout.tsx => anomaly_detection_flyout.tsx} (100%) create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index a89f82e931fd4..7a4edb8f49189 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -4,14 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import * as rt from 'io-ts'; +import { ANOMALY_THRESHOLD } from '../../infra_ml'; import { ItemTypeRT } from '../../inventory_models/types'; // TODO: Have threshold and inventory alerts import these types from this file instead of from their // local directories export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; +export const METRIC_ANOMALY_ALERT_TYPE_ID = 'metrics.alert.anomaly'; export enum Comparator { GT = '>', @@ -34,6 +35,26 @@ export enum Aggregators { P99 = 'p99', } +const metricAnomalyNodeTypeRT = rt.union([rt.literal('hosts'), rt.literal('k8s')]); +const metricAnomalyMetricRT = rt.union([ + rt.literal('memory_usage'), + rt.literal('network_in'), + rt.literal('network_out'), +]); +const metricAnomalyInfluencerFilterRT = rt.type({ + fieldName: rt.string, + fieldValue: rt.string, +}); + +export interface MetricAnomalyParams { + nodeType: rt.TypeOf; + metric: rt.TypeOf; + alertInterval?: string; + sourceId?: string; + threshold: Exclude; + influencerFilter: rt.TypeOf | undefined; +} + // Alert Preview API const baseAlertRequestParamsRT = rt.intersection([ rt.partial({ @@ -51,7 +72,6 @@ const baseAlertRequestParamsRT = rt.intersection([ rt.literal('M'), rt.literal('y'), ]), - criteria: rt.array(rt.any), alertInterval: rt.string, alertThrottle: rt.string, alertOnNoData: rt.boolean, @@ -65,6 +85,7 @@ const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([ }), rt.type({ alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID), + criteria: rt.array(rt.any), }), ]); export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf< @@ -76,15 +97,33 @@ const inventoryAlertPreviewRequestParamsRT = rt.intersection([ rt.type({ nodeType: ItemTypeRT, alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID), + criteria: rt.array(rt.any), }), ]); export type InventoryAlertPreviewRequestParams = rt.TypeOf< typeof inventoryAlertPreviewRequestParamsRT >; +const metricAnomalyAlertPreviewRequestParamsRT = rt.intersection([ + baseAlertRequestParamsRT, + rt.type({ + nodeType: metricAnomalyNodeTypeRT, + metric: metricAnomalyMetricRT, + threshold: rt.number, + alertType: rt.literal(METRIC_ANOMALY_ALERT_TYPE_ID), + }), + rt.partial({ + influencerFilter: metricAnomalyInfluencerFilterRT, + }), +]); +export type MetricAnomalyAlertPreviewRequestParams = rt.TypeOf< + typeof metricAnomalyAlertPreviewRequestParamsRT +>; + export const alertPreviewRequestParamsRT = rt.union([ metricThresholdAlertPreviewRequestParamsRT, inventoryAlertPreviewRequestParamsRT, + metricAnomalyAlertPreviewRequestParamsRT, ]); export type AlertPreviewRequestParams = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts index 589e57a1388b5..81e46d85ba220 100644 --- a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts +++ b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts @@ -5,36 +5,44 @@ * 2.0. */ -export const ML_SEVERITY_SCORES = { - warning: 3, - minor: 25, - major: 50, - critical: 75, -}; +export enum ANOMALY_SEVERITY { + CRITICAL = 'critical', + MAJOR = 'major', + MINOR = 'minor', + WARNING = 'warning', + LOW = 'low', + UNKNOWN = 'unknown', +} -export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES; +export enum ANOMALY_THRESHOLD { + CRITICAL = 75, + MAJOR = 50, + MINOR = 25, + WARNING = 3, + LOW = 0, +} -export const ML_SEVERITY_COLORS = { - critical: 'rgb(228, 72, 72)', - major: 'rgb(229, 113, 0)', - minor: 'rgb(255, 221, 0)', - warning: 'rgb(125, 180, 226)', +export const SEVERITY_COLORS = { + CRITICAL: '#fe5050', + MAJOR: '#fba740', + MINOR: '#fdec25', + WARNING: '#8bc8fb', + LOW: '#d2e9f7', + BLANK: '#ffffff', }; -export const getSeverityCategoryForScore = ( - score: number -): MLSeverityScoreCategories | undefined => { - if (score >= ML_SEVERITY_SCORES.critical) { - return 'critical'; - } else if (score >= ML_SEVERITY_SCORES.major) { - return 'major'; - } else if (score >= ML_SEVERITY_SCORES.minor) { - return 'minor'; - } else if (score >= ML_SEVERITY_SCORES.warning) { - return 'warning'; +export const getSeverityCategoryForScore = (score: number): ANOMALY_SEVERITY | undefined => { + if (score >= ANOMALY_THRESHOLD.CRITICAL) { + return ANOMALY_SEVERITY.CRITICAL; + } else if (score >= ANOMALY_THRESHOLD.MAJOR) { + return ANOMALY_SEVERITY.MAJOR; + } else if (score >= ANOMALY_THRESHOLD.MINOR) { + return ANOMALY_SEVERITY.MINOR; + } else if (score >= ANOMALY_THRESHOLD.WARNING) { + return ANOMALY_SEVERITY.WARNING; } else { // Category is too low to include - return undefined; + return ANOMALY_SEVERITY.LOW; } }; diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index fac87e20dfe7d..57c6f695453ef 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -37,7 +37,7 @@ interface Props { alertInterval: string; alertThrottle: string; alertType: PreviewableAlertTypes; - alertParams: { criteria: any[]; sourceId: string } & Record; + alertParams: { criteria?: any[]; sourceId: string } & Record; validate: (params: any) => ValidationResult; showNoDataResults?: boolean; groupByDisplayName?: string; @@ -109,6 +109,7 @@ export const AlertPreview: React.FC = (props) => { }, [previewLookbackInterval, alertInterval]); const isPreviewDisabled = useMemo(() => { + if (!alertParams.criteria) return false; const validationResult = validate({ criteria: alertParams.criteria } as any); const hasValidationErrors = Object.values(validationResult.errors).some((result) => Object.values(result).some((arr) => Array.isArray(arr) && arr.length) @@ -124,7 +125,7 @@ export const AlertPreview: React.FC = (props) => { }, [previewResult, showNoDataResults]); const hasWarningThreshold = useMemo( - () => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')), + () => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')) ?? false, [alertParams] ); diff --git a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts index a1cee1361a18f..2bb98e83cbe70 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts +++ b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts @@ -10,13 +10,15 @@ import { INFRA_ALERT_PREVIEW_PATH, METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + METRIC_ANOMALY_ALERT_TYPE_ID, AlertPreviewRequestParams, AlertPreviewSuccessResponsePayload, } from '../../../../common/alerting/metrics'; export type PreviewableAlertTypes = | typeof METRIC_THRESHOLD_ALERT_TYPE_ID - | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID; + | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID + | typeof METRIC_ANOMALY_ALERT_TYPE_ID; export async function getAlertPreview({ fetch, diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx new file mode 100644 index 0000000000000..f1236c4fc2c2b --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState, useCallback, useMemo } from 'react'; +import { + EuiPopover, + EuiButtonEmpty, + EuiContextMenu, + EuiContextMenuPanelDescriptor, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities'; +import { PrefilledInventoryAlertFlyout } from '../../inventory/components/alert_flyout'; +import { PrefilledThresholdAlertFlyout } from '../../metric_threshold/components/alert_flyout'; +import { PrefilledAnomalyAlertFlyout } from '../../metric_anomaly/components/alert_flyout'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +type VisibleFlyoutType = 'inventory' | 'threshold' | 'anomaly' | null; + +export const MetricsAlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [visibleFlyoutType, setVisibleFlyoutType] = useState(null); + const { hasInfraMLCapabilities } = useInfraMLCapabilities(); + + const closeFlyout = useCallback(() => setVisibleFlyoutType(null), [setVisibleFlyoutType]); + + const manageAlertsLinkProps = useLinkProps({ + app: 'management', + pathname: '/insightsAndAlerting/triggersActions/alerts', + }); + + const panels: EuiContextMenuPanelDescriptor[] = useMemo( + () => [ + { + id: 0, + title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', { + defaultMessage: 'Alerts', + }), + items: [ + { + name: i18n.translate('xpack.infra.alerting.infrastructureDropdownMenu', { + defaultMessage: 'Infrastructure', + }), + panel: 1, + }, + { + name: i18n.translate('xpack.infra.alerting.metricsDropdownMenu', { + defaultMessage: 'Metrics', + }), + panel: 2, + }, + { + name: i18n.translate('xpack.infra.alerting.manageAlerts', { + defaultMessage: 'Manage alerts', + }), + icon: 'tableOfContents', + onClick: manageAlertsLinkProps.onClick, + }, + ], + }, + { + id: 1, + title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', { + defaultMessage: 'Infrastructure alerts', + }), + items: [ + { + name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', { + defaultMessage: 'Create inventory alert', + }), + onClick: () => setVisibleFlyoutType('inventory'), + }, + ].concat( + hasInfraMLCapabilities + ? { + name: i18n.translate('xpack.infra.alerting.createAnomalyAlertButton', { + defaultMessage: 'Create anomaly alert', + }), + onClick: () => setVisibleFlyoutType('anomaly'), + } + : [] + ), + }, + { + id: 2, + title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', { + defaultMessage: 'Metrics alerts', + }), + items: [ + { + name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', { + defaultMessage: 'Create threshold alert', + }), + onClick: () => setVisibleFlyoutType('threshold'), + }, + ], + }, + ], + [manageAlertsLinkProps, setVisibleFlyoutType, hasInfraMLCapabilities] + ); + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + return ( + <> + + + + } + isOpen={popoverOpen} + closePopover={closePopover} + > + + + + + ); +}; + +interface AlertFlyoutProps { + visibleFlyoutType: VisibleFlyoutType; + onClose(): void; +} + +const AlertFlyout = ({ visibleFlyoutType, onClose }: AlertFlyoutProps) => { + switch (visibleFlyoutType) { + case 'inventory': + return ; + case 'threshold': + return ; + case 'anomaly': + return ; + default: + return null; + } +}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx deleted file mode 100644 index a7b6c9fb7104c..0000000000000 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useCallback } from 'react'; -import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; -import { AlertFlyout } from './alert_flyout'; -import { ManageAlertsContextMenuItem } from './manage_alerts_context_menu_item'; - -export const InventoryAlertDropdown = () => { - const [popoverOpen, setPopoverOpen] = useState(false); - const [flyoutVisible, setFlyoutVisible] = useState(false); - - const { inventoryPrefill } = useAlertPrefillContext(); - const { nodeType, metric, filterQuery } = inventoryPrefill; - - const closePopover = useCallback(() => { - setPopoverOpen(false); - }, [setPopoverOpen]); - - const openPopover = useCallback(() => { - setPopoverOpen(true); - }, [setPopoverOpen]); - - const menuItems = [ - setFlyoutVisible(true)}> - - , - , - ]; - - return ( - <> - - - - } - isOpen={popoverOpen} - closePopover={closePopover} - > - - - - - ); -}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index 815e1f2be33f2..33fe3c7af30c7 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -8,8 +8,7 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; import { InfraWaffleMapOptions } from '../../../lib/lib'; import { InventoryItemType } from '../../../../common/inventory_models/types'; import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; @@ -49,3 +48,18 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: return <>{visible && AddAlertFlyout}; }; + +export const PrefilledInventoryAlertFlyout = ({ onClose }: { onClose(): void }) => { + const { inventoryPrefill } = useAlertPrefillContext(); + const { nodeType, metric, filterQuery } = inventoryPrefill; + + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx index f02f98c49f01a..bd7812acac678 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx @@ -68,7 +68,7 @@ export const NodeTypeExpression = ({ setAggTypePopoverOpen(false)}> { + const { triggersActionsUI } = useContext(TriggerActionsContext); + + const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]); + const AddAlertFlyout = useMemo( + () => + triggersActionsUI && + triggersActionsUI.getAddAlertFlyout({ + consumer: 'infrastructure', + onClose: onCloseFlyout, + canChangeTrigger: false, + alertTypeId: METRIC_ANOMALY_ALERT_TYPE_ID, + metadata: { + metric, + nodeType, + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [triggersActionsUI, visible] + ); + + return <>{visible && AddAlertFlyout}; +}; + +export const PrefilledAnomalyAlertFlyout = ({ onClose }: { onClose(): void }) => { + const { inventoryPrefill } = useAlertPrefillContext(); + const { nodeType, metric } = inventoryPrefill; + + return ; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx new file mode 100644 index 0000000000000..ae2c6ed81badb --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` +import { coreMock as mockCoreMock } from 'src/core/public/mocks'; +import React from 'react'; +import { Expression, AlertContextMeta } from './expression'; +import { act } from 'react-dom/test-utils'; + +jest.mock('../../../containers/source/use_source_via_http', () => ({ + useSourceViaHttp: () => ({ + source: { id: 'default' }, + createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), + }), +})); + +jest.mock('../../../hooks/use_kibana', () => ({ + useKibanaContextForPlugin: () => ({ + services: mockCoreMock.createStart(), + }), +})); + +jest.mock('../../../containers/ml/infra_ml_capabilities', () => ({ + useInfraMLCapabilities: () => ({ + isLoading: false, + hasInfraMLCapabilities: true, + }), +})); + +describe('Expression', () => { + async function setup(currentOptions: AlertContextMeta) { + const alertParams = { + metric: undefined, + nodeType: undefined, + threshold: 50, + }; + const wrapper = mountWithIntl( + Reflect.set(alertParams, key, value)} + setAlertProperty={() => {}} + metadata={currentOptions} + /> + ); + + const update = async () => + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + await update(); + + return { wrapper, update, alertParams }; + } + + it('should prefill the alert using the context metadata', async () => { + const currentOptions = { + nodeType: 'pod', + metric: { type: 'tx' }, + }; + const { alertParams } = await setup(currentOptions as AlertContextMeta); + expect(alertParams.nodeType).toBe('k8s'); + expect(alertParams.metric).toBe('network_out'); + }); +}); diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx new file mode 100644 index 0000000000000..5938c7119616f --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx @@ -0,0 +1,320 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pick } from 'lodash'; +import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import { EuiFlexGroup, EuiSpacer, EuiText, EuiLoadingContent } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; +import { AlertPreview } from '../../common'; +import { + METRIC_ANOMALY_ALERT_TYPE_ID, + MetricAnomalyParams, +} from '../../../../common/alerting/metrics'; +import { euiStyled, EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import { + WhenExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { NodeTypeExpression } from './node_type'; +import { SeverityThresholdExpression } from './severity_threshold'; +import { InfraWaffleMapOptions } from '../../../lib/lib'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; + +import { validateMetricAnomaly } from './validation'; +import { InfluencerFilter } from './influencer_filter'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; + +export interface AlertContextMeta { + metric?: InfraWaffleMapOptions['metric']; + nodeType?: InventoryItemType; +} + +interface Props { + errors: IErrorObject[]; + alertParams: MetricAnomalyParams & { + sourceId: string; + }; + alertInterval: string; + alertThrottle: string; + setAlertParams(key: string, value: any): void; + setAlertProperty(key: string, value: any): void; + metadata: AlertContextMeta; +} + +export const defaultExpression = { + metric: 'memory_usage' as MetricAnomalyParams['metric'], + threshold: ANOMALY_THRESHOLD.MAJOR, + nodeType: 'hosts', + influencerFilter: undefined, +}; + +export const Expression: React.FC = (props) => { + const { hasInfraMLCapabilities, isLoading: isLoadingMLCapabilities } = useInfraMLCapabilities(); + const { http, notifications } = useKibanaContextForPlugin().services; + const { setAlertParams, alertParams, alertInterval, alertThrottle, metadata } = props; + const { source, createDerivedIndexPattern } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + fetch: http.fetch, + toastWarning: notifications.toasts.addWarning, + }); + + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + createDerivedIndexPattern, + ]); + + const [influencerFieldName, updateInfluencerFieldName] = useState( + alertParams.influencerFilter?.fieldName ?? 'host.name' + ); + + useEffect(() => { + setAlertParams('hasInfraMLCapabilities', hasInfraMLCapabilities); + }, [setAlertParams, hasInfraMLCapabilities]); + + useEffect(() => { + if (alertParams.influencerFilter) { + setAlertParams('influencerFilter', { + ...alertParams.influencerFilter, + fieldName: influencerFieldName, + }); + } + }, [influencerFieldName, alertParams, setAlertParams]); + const updateInfluencerFieldValue = useCallback( + (value: string) => { + if (value) { + setAlertParams('influencerFilter', { + ...alertParams.influencerFilter, + fieldValue: value, + }); + } else { + setAlertParams('influencerFilter', undefined); + } + }, + [setAlertParams, alertParams] + ); + + useEffect(() => { + setAlertParams('alertInterval', alertInterval); + }, [setAlertParams, alertInterval]); + + const updateNodeType = useCallback( + (nt: any) => { + setAlertParams('nodeType', nt); + }, + [setAlertParams] + ); + + const updateMetric = useCallback( + (metric: string) => { + setAlertParams('metric', metric); + }, + [setAlertParams] + ); + + const updateSeverityThreshold = useCallback( + (threshold: any) => { + setAlertParams('threshold', threshold); + }, + [setAlertParams] + ); + + const prefillNodeType = useCallback(() => { + const md = metadata; + if (md && md.nodeType) { + setAlertParams( + 'nodeType', + getMLNodeTypeFromInventoryNodeType(md.nodeType) ?? defaultExpression.nodeType + ); + } else { + setAlertParams('nodeType', defaultExpression.nodeType); + } + }, [metadata, setAlertParams]); + + const prefillMetric = useCallback(() => { + const md = metadata; + if (md && md.metric) { + setAlertParams( + 'metric', + getMLMetricFromInventoryMetric(md.metric.type) ?? defaultExpression.metric + ); + } else { + setAlertParams('metric', defaultExpression.metric); + } + }, [metadata, setAlertParams]); + + useEffect(() => { + if (!alertParams.nodeType) { + prefillNodeType(); + } + + if (!alertParams.threshold) { + setAlertParams('threshold', defaultExpression.threshold); + } + + if (!alertParams.metric) { + prefillMetric(); + } + + if (!alertParams.sourceId) { + setAlertParams('sourceId', source?.id || 'default'); + } + }, [metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + + if (isLoadingMLCapabilities) return ; + if (!hasInfraMLCapabilities) return ; + + return ( + // https://github.com/elastic/kibana/issues/89506 + + +

    + +

    +
    + + + + + + + + + + + + + + + + + + + +
    + ); +}; + +// required for dynamic import +// eslint-disable-next-line import/no-default-export +export default Expression; + +const StyledExpressionRow = euiStyled(EuiFlexGroup)` + display: flex; + flex-wrap: wrap; + margin: 0 -4px; +`; + +const StyledExpression = euiStyled.div` + padding: 0 4px; +`; + +const getDisplayNameForType = (type: InventoryItemType) => { + const inventoryModel = findInventoryModel(type); + return inventoryModel.displayName; +}; + +export const nodeTypes: { [key: string]: any } = { + hosts: { + text: getDisplayNameForType('host'), + value: 'hosts', + }, + k8s: { + text: getDisplayNameForType('pod'), + value: 'k8s', + }, +}; + +const getMLMetricFromInventoryMetric = (metric: SnapshotMetricType) => { + switch (metric) { + case 'memory': + return 'memory_usage'; + case 'tx': + return 'network_out'; + case 'rx': + return 'network_in'; + default: + return null; + } +}; + +const getMLNodeTypeFromInventoryNodeType = (nodeType: InventoryItemType) => { + switch (nodeType) { + case 'host': + return 'hosts'; + case 'pod': + return 'k8s'; + default: + return null; + } +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx new file mode 100644 index 0000000000000..34a917a77dcf5 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { debounce } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import { first } from 'lodash'; +import { EuiFlexGroup, EuiFormRow, EuiCheckbox, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { + MetricsExplorerKueryBar, + CurryLoadSuggestionsType, +} from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; + +interface Props { + fieldName: string; + fieldValue: string; + nodeType: MetricAnomalyParams['nodeType']; + onChangeFieldName: (v: string) => void; + onChangeFieldValue: (v: string) => void; + derivedIndexPattern: Parameters[0]['derivedIndexPattern']; +} + +const FILTER_TYPING_DEBOUNCE_MS = 500; + +export const InfluencerFilter = ({ + fieldName, + fieldValue, + nodeType, + onChangeFieldName, + onChangeFieldValue, + derivedIndexPattern, +}: Props) => { + const fieldNameOptions = useMemo(() => (nodeType === 'k8s' ? k8sFieldNames : hostFieldNames), [ + nodeType, + ]); + + // If initial props contain a fieldValue, assume it was passed in from loaded alertParams, + // and enable the UI element + const [isEnabled, updateIsEnabled] = useState(fieldValue ? true : false); + const [storedFieldValue, updateStoredFieldValue] = useState(fieldValue); + + useEffect( + () => + nodeType === 'k8s' + ? onChangeFieldName(first(k8sFieldNames)!.value) + : onChangeFieldName(first(hostFieldNames)!.value), + [nodeType, onChangeFieldName] + ); + + const onSelectFieldName = useCallback((e) => onChangeFieldName(e.target.value), [ + onChangeFieldName, + ]); + const onUpdateFieldValue = useCallback( + (value) => { + updateStoredFieldValue(value); + onChangeFieldValue(value); + }, + [onChangeFieldValue] + ); + + const toggleEnabled = useCallback(() => { + const nextState = !isEnabled; + updateIsEnabled(nextState); + if (!nextState) { + onChangeFieldValue(''); + } else { + onChangeFieldValue(storedFieldValue); + } + }, [isEnabled, updateIsEnabled, onChangeFieldValue, storedFieldValue]); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnUpdateFieldValue = useCallback( + debounce(onUpdateFieldValue, FILTER_TYPING_DEBOUNCE_MS), + [onUpdateFieldValue] + ); + + const affixFieldNameToQuery: CurryLoadSuggestionsType = (fn) => ( + expression, + cursorPosition, + maxSuggestions + ) => { + // Add the field name to the front of the passed-in query + const prefix = `${fieldName}:`; + // Trim whitespace to prevent AND/OR suggestions + const modifiedExpression = `${prefix}${expression}`.trim(); + // Move the cursor position forward by the length of the field name + const modifiedPosition = cursorPosition + prefix.length; + return fn(modifiedExpression, modifiedPosition, maxSuggestions, (suggestions) => + suggestions + .map((s) => ({ + ...s, + // Remove quotes from suggestions + text: s.text.replace(/\"/g, '').trim(), + // Offset the returned suggestions' cursor positions so that they can be autocompleted accurately + start: s.start - prefix.length, + end: s.end - prefix.length, + })) + // Removing quotes can lead to an already-selected suggestion still coming up in the autocomplete list, + // so filter these out + .filter((s) => !expression.startsWith(s.text)) + ); + }; + + return ( + + } + helpText={ + isEnabled ? ( + <> + {i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpText', { + defaultMessage: + 'Limit the scope of your alert trigger to anomalies influenced by certain node(s).', + })} +
    + {i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpTextExample', { + defaultMessage: 'For example: "my-node-1" or "my-node-*"', + })} + + ) : null + } + fullWidth + display="rowCompressed" + > + {isEnabled ? ( + + + + + + + + + ) : ( + <> + )} +
    + ); +}; + +const hostFieldNames = [ + { + value: 'host.name', + text: 'host.name', + }, +]; + +const k8sFieldNames = [ + { + value: 'kubernetes.pod.uid', + text: 'kubernetes.pod.uid', + }, + { + value: 'kubernetes.node.name', + text: 'kubernetes.node.name', + }, + { + value: 'kubernetes.namespace', + text: 'kubernetes.namespace', + }, +]; + +const filterByNodeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.filterByNodeLabel', { + defaultMessage: 'Filter by node', +}); diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx new file mode 100644 index 0000000000000..6ddcf8fd5cb65 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; + +type Node = MetricAnomalyParams['nodeType']; + +interface WhenExpressionProps { + value: Node; + options: { [key: string]: { text: string; value: Node } }; + onChange: (value: Node) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const NodeTypeExpression = ({ + value, + options, + onChange, + popupPosition, +}: WhenExpressionProps) => { + const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); + + return ( + { + setAggTypePopoverOpen(true); + }} + /> + } + isOpen={aggTypePopoverOpen} + closePopover={() => { + setAggTypePopoverOpen(false); + }} + ownFocus + anchorPosition={popupPosition ?? 'downLeft'} + > +
    + setAggTypePopoverOpen(false)}> + + + { + onChange(e.target.value as Node); + setAggTypePopoverOpen(false); + }} + options={Object.values(options).map((o) => o)} + /> +
    +
    + ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + + + {children} + + onClose()} + /> + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx new file mode 100644 index 0000000000000..2dc561ff172b9 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; + +interface WhenExpressionProps { + value: Exclude; + onChange: (value: ANOMALY_THRESHOLD) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +const options = { + [ANOMALY_THRESHOLD.CRITICAL]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.criticalLabel', { + defaultMessage: 'Critical', + }), + value: ANOMALY_THRESHOLD.CRITICAL, + }, + [ANOMALY_THRESHOLD.MAJOR]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.majorLabel', { + defaultMessage: 'Major', + }), + value: ANOMALY_THRESHOLD.MAJOR, + }, + [ANOMALY_THRESHOLD.MINOR]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.minorLabel', { + defaultMessage: 'Minor', + }), + value: ANOMALY_THRESHOLD.MINOR, + }, + [ANOMALY_THRESHOLD.WARNING]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.warningLabel', { + defaultMessage: 'Warning', + }), + value: ANOMALY_THRESHOLD.WARNING, + }, +}; + +export const SeverityThresholdExpression = ({ + value, + onChange, + popupPosition, +}: WhenExpressionProps) => { + const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); + + return ( + { + setAggTypePopoverOpen(true); + }} + /> + } + isOpen={aggTypePopoverOpen} + closePopover={() => { + setAggTypePopoverOpen(false); + }} + ownFocus + anchorPosition={popupPosition ?? 'downLeft'} + > +
    + setAggTypePopoverOpen(false)}> + + + { + onChange(Number(e.target.value) as ANOMALY_THRESHOLD); + setAggTypePopoverOpen(false); + }} + options={Object.values(options).map((o) => o)} + /> +
    +
    + ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + + + {children} + + onClose()} + /> + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx new file mode 100644 index 0000000000000..8e254fb2b67a8 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; + +export function validateMetricAnomaly({ + hasInfraMLCapabilities, +}: { + hasInfraMLCapabilities: boolean; +}): ValidationResult { + const validationResult = { errors: {} }; + const errors: { + hasInfraMLCapabilities: string[]; + } = { + hasInfraMLCapabilities: [], + }; + + validationResult.errors = errors; + + if (!hasInfraMLCapabilities) { + errors.hasInfraMLCapabilities.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.mlCapabilitiesRequired', { + defaultMessage: 'Cannot create an anomaly alert when machine learning is disabled.', + }) + ); + } + + return validationResult; +} diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts new file mode 100644 index 0000000000000..31fed514bdacc --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../common/alerting/metrics'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { AlertTypeParams } from '../../../../alerts/common'; +import { validateMetricAnomaly } from './components/validation'; + +interface MetricAnomalyAlertTypeParams extends AlertTypeParams { + hasInfraMLCapabilities: boolean; +} + +export function createMetricAnomalyAlertType(): AlertTypeModel { + return { + id: METRIC_ANOMALY_ALERT_TYPE_ID, + description: i18n.translate('xpack.infra.metrics.anomaly.alertFlyout.alertDescription', { + defaultMessage: 'Alert when the anomaly score exceeds a defined threshold.', + }), + iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/metric-anomaly-alert.html`; + }, + alertParamsExpression: React.lazy(() => import('./components/expression')), + validate: validateMetricAnomaly, + defaultActionMessage: i18n.translate( + 'xpack.infra.metrics.alerting.anomaly.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} is in a state of \\{\\{context.alertState\\}\\} + +\\{\\{context.metric\\}\\} was \\{\\{context.summary\\}\\} than normal at \\{\\{context.timestamp\\}\\} + +Typical value: \\{\\{context.typical\\}\\} +Actual value: \\{\\{context.actual\\}\\} +`, + } + ), + requiresAppContext: false, + }; +} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx deleted file mode 100644 index 3bbe811225825..0000000000000 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ /dev/null @@ -1,57 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useCallback } from 'react'; -import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useAlertPrefillContext } from '../../use_alert_prefill'; -import { AlertFlyout } from './alert_flyout'; -import { ManageAlertsContextMenuItem } from '../../inventory/components/manage_alerts_context_menu_item'; - -export const MetricsAlertDropdown = () => { - const [popoverOpen, setPopoverOpen] = useState(false); - const [flyoutVisible, setFlyoutVisible] = useState(false); - - const { metricThresholdPrefill } = useAlertPrefillContext(); - const { groupBy, filterQuery, metrics } = metricThresholdPrefill; - - const closePopover = useCallback(() => { - setPopoverOpen(false); - }, [setPopoverOpen]); - - const openPopover = useCallback(() => { - setPopoverOpen(true); - }, [setPopoverOpen]); - - const menuItems = [ - setFlyoutVisible(true)}> - - , - , - ]; - - return ( - <> - - - - } - isOpen={popoverOpen} - closePopover={closePopover} - > - - - - - ); -}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index 929654ecb4693..e7e4ade5257fc 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -7,10 +7,10 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; interface Props { visible?: boolean; @@ -42,3 +42,10 @@ export const AlertFlyout = (props: Props) => { return <>{visible && AddAlertFlyout}; }; + +export const PrefilledThresholdAlertFlyout = ({ onClose }: { onClose(): void }) => { + const { metricThresholdPrefill } = useAlertPrefillContext(); + const { groupBy, filterQuery, metrics } = metricThresholdPrefill; + + return ; +}; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts index 1bcc9e7157a51..db5a996c604fc 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts @@ -14,4 +14,3 @@ export * from './missing_results_privileges_prompt'; export * from './missing_setup_privileges_prompt'; export * from './ml_unavailable_prompt'; export * from './setup_status_unknown_prompt'; -export * from './subscription_splash_content'; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx deleted file mode 100644 index c91c1d82afe9b..0000000000000 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx +++ /dev/null @@ -1,176 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, - EuiText, - EuiButton, - EuiButtonEmpty, - EuiImage, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { HttpStart } from 'src/core/public'; -import { LoadingPage } from '../../loading_page'; - -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { useTrialStatus } from '../../../hooks/use_trial_status'; - -export const SubscriptionSplashContent: React.FC = () => { - const { services } = useKibana<{ http: HttpStart }>(); - const { loadState, isTrialAvailable, checkTrialAvailability } = useTrialStatus(); - - useEffect(() => { - checkTrialAvailability(); - }, [checkTrialAvailability]); - - if (loadState === 'pending') { - return ( - - ); - } - - const canStartTrial = isTrialAvailable && loadState === 'resolved'; - - let title; - let description; - let cta; - - if (canStartTrial) { - title = ( - - ); - - description = ( - - ); - - cta = ( - - - - ); - } else { - title = ( - - ); - - description = ( - - ); - - cta = ( - - - - ); - } - - return ( - - - - - - -

    {title}

    -
    - - -

    {description}

    -
    - -
    {cta}
    -
    - - - -
    - - -

    - -

    -
    - - - -
    -
    -
    -
    - ); -}; - -const SubscriptionPage = euiStyled(EuiPage)` - height: 100% -`; - -const SubscriptionPageContent = euiStyled(EuiPageContent)` - max-width: 768px !important; -`; - -const SubscriptionPageFooter = euiStyled.div` - background: ${(props) => props.theme.eui.euiColorLightestShade}; - margin: 0 -${(props) => props.theme.eui.paddingSizes.l} -${(props) => - props.theme.eui.paddingSizes.l}; - padding: ${(props) => props.theme.eui.paddingSizes.l}; -`; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index 4b609a881bd18..e63f43470497d 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -75,7 +75,7 @@ export const SourceConfigurationSettings = ({ source, ]); - const { hasInfraMLCapabilites } = useInfraMLCapabilitiesContext(); + const { hasInfraMLCapabilities } = useInfraMLCapabilitiesContext(); if ((isLoading || isUninitialized) && !source) { return ; @@ -128,7 +128,7 @@ export const SourceConfigurationSettings = ({ /> - {hasInfraMLCapabilites && ( + {hasInfraMLCapabilities && ( <> { const { services } = useKibana<{ http: HttpStart }>(); @@ -102,58 +102,60 @@ export const SubscriptionSplashContent: React.FC = () => { } return ( - - - - - - -

    {title}

    + + + + + + + +

    {title}

    +
    + + +

    {description}

    +
    + +
    {cta}
    +
    + + + +
    + + +

    + +

    - - -

    {description}

    -
    - -
    {cta}
    -
    - - - -
    - - -

    + -

    -
    - - - -
    -
    -
    -
    + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx index 72dc4da01d867..661ce8f8a253c 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx @@ -52,11 +52,11 @@ export const useInfraMLCapabilities = () => { const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob; const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs; - const hasInfraMLCapabilites = + const hasInfraMLCapabilities = mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace; return { - hasInfraMLCapabilites, + hasInfraMLCapabilities, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities, isLoading, diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx index 379ac9774c242..1a759950f640d 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -56,7 +56,8 @@ class WithKueryAutocompletionComponent extends React.Component< private loadSuggestions = async ( expression: string, cursorPosition: number, - maxSuggestions?: number + maxSuggestions?: number, + transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[] ) => { const { indexPattern } = this.props; const language = 'kuery'; @@ -86,6 +87,10 @@ class WithKueryAutocompletionComponent extends React.Component< boolFilter: [], })) || []; + const transformedSuggestions = transformSuggestions + ? transformSuggestions(suggestions) + : suggestions; + this.setState((state) => state.currentRequest && state.currentRequest.expression !== expression && @@ -94,7 +99,9 @@ class WithKueryAutocompletionComponent extends React.Component< : { ...state, currentRequest: null, - suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions, + suggestions: maxSuggestions + ? transformedSuggestions.slice(0, maxSuggestions) + : transformedSuggestions, } ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index f0fdd79bcd93d..628df397998ee 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback, useEffect } from 'react'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, - SubscriptionSplashContent, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index 4d06d23ef93ef..5fd00527b8b70 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import React, { memo, useEffect, useCallback } from 'react'; +import { SubscriptionSplashContent } from '../../../components/subscription_splash_content'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, - SubscriptionSplashContent, } from '../../../components/logging/log_analysis_setup'; import { LogAnalysisSetupFlyout, diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 52c2a70f2d359..8fd32bda7fbc8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -35,12 +35,11 @@ import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; -import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown'; -import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; +import { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown'; import { SavedView } from '../../containers/saved_view/saved_view'; import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities'; -import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout'; +import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout'; import { HeaderMenuPortal } from '../../../../observability/public'; import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; @@ -83,8 +82,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { - - + { jobSummaries: k8sJobSummaries, } = useMetricK8sModuleContext(); const { - hasInfraMLCapabilites, + hasInfraMLCapabilities, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities, } = useInfraMLCapabilitiesContext(); @@ -69,7 +69,7 @@ export const FlyoutHome = (props: Props) => { } }, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]); - if (!hasInfraMLCapabilites) { + if (!hasInfraMLCapabilities) { return ; } else if (!hasInfraMLReadCapabilities) { return ; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx index 44391568741f3..e22c6fa661181 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx @@ -10,7 +10,19 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../../../../components/autocomplete_field'; -import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { + esKuery, + IIndexPattern, + QuerySuggestion, +} from '../../../../../../../../src/plugins/data/public'; + +type LoadSuggestionsFn = ( + e: string, + p: number, + m?: number, + transform?: (s: QuerySuggestion[]) => QuerySuggestion[] +) => void; +export type CurryLoadSuggestionsType = (loadSuggestions: LoadSuggestionsFn) => LoadSuggestionsFn; interface Props { derivedIndexPattern: IIndexPattern; @@ -18,6 +30,7 @@ interface Props { onChange?: (query: string) => void; value?: string | null; placeholder?: string; + curryLoadSuggestions?: CurryLoadSuggestionsType; } function validateQuery(query: string) { @@ -35,6 +48,7 @@ export const MetricsExplorerKueryBar = ({ onChange, value, placeholder, + curryLoadSuggestions = defaultCurryLoadSuggestions, }: Props) => { const [draftQuery, setDraftQuery] = useState(value || ''); const [isValid, setValidation] = useState(true); @@ -73,7 +87,7 @@ export const MetricsExplorerKueryBar = ({ aria-label={placeholder} isLoadingSuggestions={isLoadingSuggestions} isValid={isValid} - loadSuggestions={loadSuggestions} + loadSuggestions={curryLoadSuggestions(loadSuggestions)} onChange={handleChange} onSubmit={onSubmit} placeholder={placeholder || defaultPlaceholder} @@ -84,3 +98,6 @@ export const MetricsExplorerKueryBar = ({ ); }; + +const defaultCurryLoadSuggestions: CurryLoadSuggestionsType = (loadSuggestions) => (...args) => + loadSuggestions(...args); diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 8e7d165f8a535..d4bb83e8668ba 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -10,6 +10,7 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { createMetricThresholdAlertType } from './alerting/metric_threshold'; import { createInventoryMetricAlertType } from './alerting/inventory'; +import { createMetricAnomalyAlertType } from './alerting/metric_anomaly'; import { getAlertType as getLogsAlertType } from './alerting/log_threshold'; import { registerFeatures } from './register_feature'; import { @@ -35,6 +36,7 @@ export class Plugin implements InfraClientPluginClass { pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType()); pluginsSetup.triggersActionsUi.alertTypeRegistry.register(getLogsAlertType()); pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType()); + pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricAnomalyAlertType()); if (pluginsSetup.observability) { pluginsSetup.observability.dashboard.register({ diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index b18b6e8a6eba6..4d70676d25e40 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -23,7 +23,7 @@ import type { ObservabilityPluginStart, } from '../../observability/public'; import type { SpacesPluginStart } from '../../spaces/public'; -import { MlPluginStart } from '../../ml/public'; +import { MlPluginStart, MlPluginSetup } from '../../ml/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; // Our own setup and start contract values @@ -36,6 +36,7 @@ export interface InfraClientSetupDeps { observability: ObservabilityPluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionSetup; + ml: MlPluginSetup; embeddable: EmbeddableSetup; } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts new file mode 100644 index 0000000000000..b7ef8ec7d2312 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; +import { getMetricsHostsAnomalies, getMetricK8sAnomalies } from '../../infra_ml'; +import { MlSystem, MlAnomalyDetectors } from '../../../types'; + +type ConditionParams = Omit & { + spaceId: string; + startTime: number; + endTime: number; + mlSystem: MlSystem; + mlAnomalyDetectors: MlAnomalyDetectors; +}; + +export const evaluateCondition = async ({ + nodeType, + spaceId, + sourceId, + mlSystem, + mlAnomalyDetectors, + startTime, + endTime, + metric, + threshold, + influencerFilter, +}: ConditionParams) => { + const getAnomalies = nodeType === 'k8s' ? getMetricK8sAnomalies : getMetricsHostsAnomalies; + + const result = await getAnomalies( + { + spaceId, + mlSystem, + mlAnomalyDetectors, + }, + sourceId ?? 'default', + threshold, + startTime, + endTime, + metric, + { field: 'anomalyScore', direction: 'desc' }, + { pageSize: 100 }, + influencerFilter + ); + + return result; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts new file mode 100644 index 0000000000000..ec95aac7268ad --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { first } from 'lodash'; +import moment from 'moment'; +import { stateToAlertMessage } from '../common/messages'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; +import { MappedAnomalyHit } from '../../infra_ml'; +import { AlertStates } from '../common/types'; +import { + ActionGroup, + AlertInstanceContext, + AlertInstanceState, +} from '../../../../../alerts/common'; +import { AlertExecutorOptions } from '../../../../../alerts/server'; +import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { MetricAnomalyAllowedActionGroups } from './register_metric_anomaly_alert_type'; +import { MlPluginSetup } from '../../../../../ml/server'; +import { KibanaRequest } from '../../../../../../../src/core/server'; +import { InfraBackendLibs } from '../../infra_types'; +import { evaluateCondition } from './evaluate_condition'; + +export const createMetricAnomalyExecutor = (libs: InfraBackendLibs, ml?: MlPluginSetup) => async ({ + services, + params, + startedAt, +}: AlertExecutorOptions< + /** + * TODO: Remove this use of `any` by utilizing a proper type + */ + Record, + Record, + AlertInstanceState, + AlertInstanceContext, + MetricAnomalyAllowedActionGroups +>) => { + if (!ml) { + return; + } + const request = {} as KibanaRequest; + const mlSystem = ml.mlSystemProvider(request, services.savedObjectsClient); + const mlAnomalyDetectors = ml.anomalyDetectorsProvider(request, services.savedObjectsClient); + + const { + metric, + alertInterval, + influencerFilter, + sourceId, + nodeType, + threshold, + } = params as MetricAnomalyParams; + + const alertInstance = services.alertInstanceFactory(`${nodeType}-${metric}`); + + const bucketInterval = getIntervalInSeconds('15m') * 1000; + const alertIntervalInMs = getIntervalInSeconds(alertInterval ?? '1m') * 1000; + + const endTime = startedAt.getTime(); + // Anomalies are bucketed at :00, :15, :30, :45 minutes every hour + const previousBucketStartTime = endTime - (endTime % bucketInterval); + + // If the alert interval is less than 15m, make sure that it actually queries an anomaly bucket + const startTime = Math.min(endTime - alertIntervalInMs, previousBucketStartTime); + + const { data } = await evaluateCondition({ + sourceId: sourceId ?? 'default', + spaceId: 'default', + mlSystem, + mlAnomalyDetectors, + startTime, + endTime, + metric, + threshold, + nodeType, + influencerFilter, + }); + + const shouldAlertFire = data.length > 0; + + if (shouldAlertFire) { + const { startTime: anomalyStartTime, anomalyScore, actual, typical, influencers } = first( + data as MappedAnomalyHit[] + )!; + + alertInstance.scheduleActions(FIRED_ACTIONS_ID, { + alertState: stateToAlertMessage[AlertStates.ALERT], + timestamp: moment(anomalyStartTime).toISOString(), + anomalyScore, + actual, + typical, + metric: metricNameMap[metric], + summary: generateSummaryMessage(actual, typical), + influencers: influencers.join(', '), + }); + } +}; + +export const FIRED_ACTIONS_ID = 'metrics.anomaly.fired'; +export const FIRED_ACTIONS: ActionGroup = { + id: FIRED_ACTIONS_ID, + name: i18n.translate('xpack.infra.metrics.alerting.anomaly.fired', { + defaultMessage: 'Fired', + }), +}; + +const generateSummaryMessage = (actual: number, typical: number) => { + const differential = (Math.max(actual, typical) / Math.min(actual, typical)) + .toFixed(1) + .replace('.0', ''); + if (actual > typical) { + return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryHigher', { + defaultMessage: '{differential}x higher', + values: { + differential, + }, + }); + } else { + return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryLower', { + defaultMessage: '{differential}x lower', + values: { + differential, + }, + }); + } +}; + +const metricNameMap = { + memory_usage: i18n.translate('xpack.infra.metrics.alerting.anomaly.memoryUsage', { + defaultMessage: 'Memory usage', + }), + network_in: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkIn', { + defaultMessage: 'Network in', + }), + network_out: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkOut', { + defaultMessage: 'Network out', + }), +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts new file mode 100644 index 0000000000000..98992701e3bb4 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Unit } from '@elastic/datemath'; +import { countBy } from 'lodash'; +import { MappedAnomalyHit } from '../../infra_ml'; +import { MlSystem, MlAnomalyDetectors } from '../../../types'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; +import { + TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, + isTooManyBucketsPreviewException, +} from '../../../../common/alerting/metrics'; +import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { evaluateCondition } from './evaluate_condition'; + +interface PreviewMetricAnomalyAlertParams { + mlSystem: MlSystem; + mlAnomalyDetectors: MlAnomalyDetectors; + spaceId: string; + params: MetricAnomalyParams; + sourceId: string; + lookback: Unit; + alertInterval: string; + alertThrottle: string; + alertOnNoData: boolean; +} + +export const previewMetricAnomalyAlert = async ({ + mlSystem, + mlAnomalyDetectors, + spaceId, + params, + sourceId, + lookback, + alertInterval, + alertThrottle, +}: PreviewMetricAnomalyAlertParams) => { + const { metric, threshold, influencerFilter, nodeType } = params as MetricAnomalyParams; + + const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); + const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle); + const executionsPerThrottle = Math.floor(throttleIntervalInSeconds / alertIntervalInSeconds); + + const lookbackInterval = `1${lookback}`; + const lookbackIntervalInSeconds = getIntervalInSeconds(lookbackInterval); + const endTime = Date.now(); + const startTime = endTime - lookbackIntervalInSeconds * 1000; + + const numberOfExecutions = Math.floor(lookbackIntervalInSeconds / alertIntervalInSeconds); + const bucketIntervalInSeconds = getIntervalInSeconds('15m'); + const bucketsPerExecution = Math.max( + 1, + Math.floor(alertIntervalInSeconds / bucketIntervalInSeconds) + ); + + try { + let anomalies: MappedAnomalyHit[] = []; + const { data } = await evaluateCondition({ + nodeType, + spaceId, + sourceId, + mlSystem, + mlAnomalyDetectors, + startTime, + endTime, + metric, + threshold, + influencerFilter, + }); + anomalies = [...anomalies, ...data]; + + const anomaliesByTime = countBy(anomalies, ({ startTime: anomStartTime }) => anomStartTime); + + let numberOfTimesFired = 0; + let numberOfNotifications = 0; + let throttleTracker = 0; + const notifyWithThrottle = () => { + if (throttleTracker === 0) numberOfNotifications++; + throttleTracker++; + }; + // Mock each alert evaluation + for (let i = 0; i < numberOfExecutions; i++) { + const executionTime = startTime + alertIntervalInSeconds * 1000 * i; + // Get an array of bucket times this mock alert evaluation will be looking at + // Anomalies are bucketed at :00, :15, :30, :45 minutes every hour, + // so this is an array of how many of those times occurred between this evaluation + // and the previous one + const bucketsLookedAt = Array.from(Array(bucketsPerExecution), (_, idx) => { + const previousBucketStartTime = + executionTime - + (executionTime % (bucketIntervalInSeconds * 1000)) - + idx * bucketIntervalInSeconds * 1000; + return previousBucketStartTime; + }); + const anomaliesDetectedInBuckets = bucketsLookedAt.some((bucketTime) => + Reflect.has(anomaliesByTime, bucketTime) + ); + + if (anomaliesDetectedInBuckets) { + numberOfTimesFired++; + notifyWithThrottle(); + } else if (throttleTracker > 0) { + throttleTracker++; + } + if (throttleTracker === executionsPerThrottle) { + throttleTracker = 0; + } + } + + return { fired: numberOfTimesFired, notifications: numberOfNotifications }; + } catch (e) { + if (!isTooManyBucketsPreviewException(e)) throw e; + const { maxBuckets } = e; + throw new Error(`${TOO_MANY_BUCKETS_PREVIEW_EXCEPTION}:${maxBuckets}`); + } +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts new file mode 100644 index 0000000000000..8ac62c125515a --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { MlPluginSetup } from '../../../../../ml/server'; +import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../../alerts/server'; +import { + createMetricAnomalyExecutor, + FIRED_ACTIONS, + FIRED_ACTIONS_ID, +} from './metric_anomaly_executor'; +import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; +import { InfraBackendLibs } from '../../infra_types'; +import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils'; +import { alertStateActionVariableDescription } from '../common/messages'; +import { RecoveredActionGroupId } from '../../../../../alerts/common'; + +export type MetricAnomalyAllowedActionGroups = typeof FIRED_ACTIONS_ID; + +export const registerMetricAnomalyAlertType = ( + libs: InfraBackendLibs, + ml?: MlPluginSetup +): AlertType< + /** + * TODO: Remove this use of `any` by utilizing a proper type + */ + Record, + Record, + AlertInstanceState, + AlertInstanceContext, + MetricAnomalyAllowedActionGroups, + RecoveredActionGroupId +> => ({ + id: METRIC_ANOMALY_ALERT_TYPE_ID, + name: i18n.translate('xpack.infra.metrics.anomaly.alertName', { + defaultMessage: 'Infrastructure anomaly', + }), + validate: { + params: schema.object( + { + nodeType: oneOfLiterals(['hosts', 'k8s']), + alertInterval: schema.string(), + metric: oneOfLiterals(['memory_usage', 'network_in', 'network_out']), + threshold: schema.number(), + filterQuery: schema.maybe( + schema.string({ validate: validateIsStringElasticsearchJSONFilter }) + ), + sourceId: schema.string(), + }, + { unknowns: 'allow' } + ), + }, + defaultActionGroupId: FIRED_ACTIONS_ID, + actionGroups: [FIRED_ACTIONS], + producer: 'infrastructure', + minimumLicenseRequired: 'basic', + executor: createMetricAnomalyExecutor(libs, ml), + actionVariables: { + context: [ + { name: 'alertState', description: alertStateActionVariableDescription }, + { + name: 'metric', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyMetricDescription', { + defaultMessage: 'The metric name in the specified condition.', + }), + }, + { + name: 'timestamp', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyTimestampDescription', { + defaultMessage: 'A timestamp of when the anomaly was detected.', + }), + }, + { + name: 'anomalyScore', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyScoreDescription', { + defaultMessage: 'The exact severity score of the detected anomaly.', + }), + }, + { + name: 'actual', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyActualDescription', { + defaultMessage: 'The actual value of the monitored metric at the time of the anomaly.', + }), + }, + { + name: 'typical', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyTypicalDescription', { + defaultMessage: 'The typical value of the monitored metric at the time of the anomaly.', + }), + }, + { + name: 'summary', + description: i18n.translate('xpack.infra.metrics.alerting.anomalySummaryDescription', { + defaultMessage: 'A description of the anomaly, e.g. "2x higher."', + }), + }, + { + name: 'influencers', + description: i18n.translate('xpack.infra.metrics.alerting.anomalyInfluencersDescription', { + defaultMessage: 'A list of node names that influenced the anomaly.', + }), + }, + ], + }, +}); diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index 0b4df6805759e..11fbe269b854d 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -8,13 +8,21 @@ import { PluginSetupContract } from '../../../../alerts/server'; import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type'; +import { registerMetricAnomalyAlertType } from './metric_anomaly/register_metric_anomaly_alert_type'; + import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type'; import { InfraBackendLibs } from '../infra_types'; +import { MlPluginSetup } from '../../../../ml/server'; -const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => { +const registerAlertTypes = ( + alertingPlugin: PluginSetupContract, + libs: InfraBackendLibs, + ml?: MlPluginSetup +) => { if (alertingPlugin) { alertingPlugin.registerType(registerMetricThresholdAlertType(libs)); alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs)); + alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml)); const registerFns = [registerLogThresholdAlertType]; registerFns.forEach((fn) => { diff --git a/x-pack/plugins/infra/server/lib/infra_ml/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/common.ts index 0182cb0e4099a..686f27d714cc1 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/common.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/common.ts @@ -17,6 +17,23 @@ import { import { decodeOrThrow } from '../../../common/runtime_types'; import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing'; +export interface MappedAnomalyHit { + id: string; + anomalyScore: number; + typical: number; + actual: number; + jobId: string; + startTime: number; + duration: number; + influencers: string[]; + categoryId?: string; +} + +export interface InfluencerFilter { + fieldName: string; + fieldValue: string; +} + export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) { const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES'); const { diff --git a/x-pack/plugins/infra/server/lib/infra_ml/index.ts b/x-pack/plugins/infra/server/lib/infra_ml/index.ts index d346b71d76aa8..82093b1a359d0 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/index.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/index.ts @@ -8,3 +8,4 @@ export * from './errors'; export * from './metrics_hosts_anomalies'; export * from './metrics_k8s_anomalies'; +export { MappedAnomalyHit } from './common'; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts index 7873fd8e43a7b..f6e11f5294191 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts @@ -5,11 +5,10 @@ * 2.0. */ -import type { InfraPluginRequestHandlerContext } from '../../types'; import { InfraRequestHandlerContext } from '../../types'; import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob } from './common'; -import { getJobId, metricsHostsJobTypes } from '../../../common/infra_ml'; +import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common'; +import { getJobId, metricsHostsJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml'; import { Sort, Pagination } from '../../../common/http_api/infra_ml'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; @@ -19,18 +18,6 @@ import { createMetricsHostsAnomaliesQuery, } from './queries/metrics_hosts_anomalies'; -interface MappedAnomalyHit { - id: string; - anomalyScore: number; - typical: number; - actual: number; - jobId: string; - startTime: number; - duration: number; - influencers: string[]; - categoryId?: string; -} - async function getCompatibleAnomaliesJobIds( spaceId: string, sourceId: string, @@ -74,14 +61,15 @@ async function getCompatibleAnomaliesJobIds( } export async function getMetricsHostsAnomalies( - context: InfraPluginRequestHandlerContext & { infra: Required }, + context: Required, sourceId: string, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter ) { const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies'); @@ -89,10 +77,10 @@ export async function getMetricsHostsAnomalies( jobIds, timing: { spans: jobSpans }, } = await getCompatibleAnomaliesJobIds( - context.infra.spaceId, + context.spaceId, sourceId, metric, - context.infra.mlAnomalyDetectors + context.mlAnomalyDetectors ); if (jobIds.length === 0) { @@ -108,13 +96,14 @@ export async function getMetricsHostsAnomalies( hasMoreEntries, timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricsHostsAnomalies( - context.infra.mlSystem, + context.mlSystem, anomalyThreshold, jobIds, startTime, endTime, sort, - pagination + pagination, + influencerFilter ); const data = anomalies.map((anomaly) => { @@ -164,12 +153,13 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricsHostsAnomalies( mlSystem: MlSystem, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, jobIds: string[], startTime: number, endTime: number, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter ) { // We'll request 1 extra entry on top of our pageSize to determine if there are // more entries to be fetched. This avoids scenarios where the client side can't @@ -188,6 +178,7 @@ async function fetchMetricsHostsAnomalies( endTime, sort, pagination: expandedPagination, + influencerFilter, }), jobIds ) diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts index 0c87b2f0f8b53..34039e9107f00 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts @@ -5,11 +5,10 @@ * 2.0. */ -import type { InfraPluginRequestHandlerContext } from '../../types'; import { InfraRequestHandlerContext } from '../../types'; import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob } from './common'; -import { getJobId, metricsK8SJobTypes } from '../../../common/infra_ml'; +import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common'; +import { getJobId, metricsK8SJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml'; import { Sort, Pagination } from '../../../common/http_api/infra_ml'; import type { MlSystem, MlAnomalyDetectors } from '../../types'; import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; @@ -19,18 +18,6 @@ import { createMetricsK8sAnomaliesQuery, } from './queries/metrics_k8s_anomalies'; -interface MappedAnomalyHit { - id: string; - anomalyScore: number; - typical: number; - actual: number; - jobId: string; - startTime: number; - influencers: string[]; - duration: number; - categoryId?: string; -} - async function getCompatibleAnomaliesJobIds( spaceId: string, sourceId: string, @@ -74,14 +61,15 @@ async function getCompatibleAnomaliesJobIds( } export async function getMetricK8sAnomalies( - context: InfraPluginRequestHandlerContext & { infra: Required }, + context: Required, sourceId: string, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter ) { const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies'); @@ -89,10 +77,10 @@ export async function getMetricK8sAnomalies( jobIds, timing: { spans: jobSpans }, } = await getCompatibleAnomaliesJobIds( - context.infra.spaceId, + context.spaceId, sourceId, metric, - context.infra.mlAnomalyDetectors + context.mlAnomalyDetectors ); if (jobIds.length === 0) { @@ -107,13 +95,14 @@ export async function getMetricK8sAnomalies( hasMoreEntries, timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricK8sAnomalies( - context.infra.mlSystem, + context.mlSystem, anomalyThreshold, jobIds, startTime, endTime, sort, - pagination + pagination, + influencerFilter ); const data = anomalies.map((anomaly) => { @@ -160,12 +149,13 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricK8sAnomalies( mlSystem: MlSystem, - anomalyThreshold: number, + anomalyThreshold: ANOMALY_THRESHOLD, jobIds: string[], startTime: number, endTime: number, sort: Sort, - pagination: Pagination + pagination: Pagination, + influencerFilter?: InfluencerFilter | undefined ) { // We'll request 1 extra entry on top of our pageSize to determine if there are // more entries to be fetched. This avoids scenarios where the client side can't @@ -184,6 +174,7 @@ async function fetchMetricK8sAnomalies( endTime, sort, pagination: expandedPagination, + influencerFilter, }), jobIds ) diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts index b3676fc54aeaa..6f996a672a44a 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts @@ -77,3 +77,35 @@ export const createDatasetsFilters = (datasets?: string[]) => }, ] : []; + +export const createInfluencerFilter = ({ + fieldName, + fieldValue, +}: { + fieldName: string; + fieldValue: string; +}) => [ + { + nested: { + path: 'influencers', + query: { + bool: { + must: [ + { + match: { + 'influencers.influencer_field_name': fieldName, + }, + }, + { + query_string: { + fields: ['influencers.influencer_field_values'], + query: fieldValue, + minimum_should_match: 1, + }, + }, + ], + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts index 45587cd258e5d..7808851508a7c 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { createJobIdsFilters, @@ -13,7 +14,9 @@ import { createResultTypeFilters, defaultRequestParameters, createAnomalyScoreFilter, + createInfluencerFilter, } from './common'; +import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; // TODO: Reassess validity of this against ML docs @@ -32,13 +35,15 @@ export const createMetricsHostsAnomaliesQuery = ({ endTime, sort, pagination, + influencerFilter, }: { jobIds: string[]; - anomalyThreshold: number; + anomalyThreshold: ANOMALY_THRESHOLD; startTime: number; endTime: number; sort: Sort; pagination: Pagination; + influencerFilter?: InfluencerFilter; }) => { const { field } = sort; const { pageSize } = pagination; @@ -50,6 +55,10 @@ export const createMetricsHostsAnomaliesQuery = ({ ...createResultTypeFilters(['record']), ]; + const influencerQuery = influencerFilter + ? { must: createInfluencerFilter(influencerFilter) } + : {}; + const sourceFields = [ 'job_id', 'record_score', @@ -77,6 +86,7 @@ export const createMetricsHostsAnomaliesQuery = ({ query: { bool: { filter: filters, + ...influencerQuery, }, }, search_after: queryCursor, diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts index 56a4b99e7236c..54eea067177ed 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { createJobIdsFilters, @@ -13,7 +14,9 @@ import { createResultTypeFilters, defaultRequestParameters, createAnomalyScoreFilter, + createInfluencerFilter, } from './common'; +import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; // TODO: Reassess validity of this against ML docs @@ -32,13 +35,15 @@ export const createMetricsK8sAnomaliesQuery = ({ endTime, sort, pagination, + influencerFilter, }: { jobIds: string[]; - anomalyThreshold: number; + anomalyThreshold: ANOMALY_THRESHOLD; startTime: number; endTime: number; sort: Sort; pagination: Pagination; + influencerFilter?: InfluencerFilter; }) => { const { field } = sort; const { pageSize } = pagination; @@ -50,6 +55,10 @@ export const createMetricsK8sAnomaliesQuery = ({ ...createResultTypeFilters(['record']), ]; + const influencerQuery = influencerFilter + ? { must: createInfluencerFilter(influencerFilter) } + : {}; + const sourceFields = [ 'job_id', 'record_score', @@ -76,6 +85,7 @@ export const createMetricsK8sAnomaliesQuery = ({ query: { bool: { filter: filters, + ...influencerQuery, }, }, search_after: queryCursor, diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 99555fa56acd5..0ac49e05b36b9 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -137,7 +137,7 @@ export class InfraServerPlugin implements Plugin { ]); initInfraServer(this.libs); - registerAlertTypes(plugins.alerts, this.libs); + registerAlertTypes(plugins.alerts, this.libs, plugins.ml); core.http.registerRouteHandlerContext( 'infra', diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index cc2cf4092520a..3da560135eaf4 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -9,17 +9,21 @@ import { PreviewResult } from '../../lib/alerting/common/types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + METRIC_ANOMALY_ALERT_TYPE_ID, INFRA_ALERT_PREVIEW_PATH, TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, alertPreviewRequestParamsRT, alertPreviewSuccessResponsePayloadRT, MetricThresholdAlertPreviewRequestParams, InventoryAlertPreviewRequestParams, + MetricAnomalyAlertPreviewRequestParams, } from '../../../common/alerting/metrics'; import { createValidationFunction } from '../../../common/runtime_types'; import { previewInventoryMetricThresholdAlert } from '../../lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert'; import { previewMetricThresholdAlert } from '../../lib/alerting/metric_threshold/preview_metric_threshold_alert'; +import { previewMetricAnomalyAlert } from '../../lib/alerting/metric_anomaly/preview_metric_anomaly_alert'; import { InfraBackendLibs } from '../../lib/infra_types'; +import { assertHasInfraMlPlugins } from '../../utils/request_context'; export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => { const { callWithRequest } = framework; @@ -33,8 +37,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }, framework.router.handleLegacyErrors(async (requestContext, request, response) => { const { - criteria, - filterQuery, lookback, sourceId, alertType, @@ -55,7 +57,11 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) try { switch (alertType) { case METRIC_THRESHOLD_ALERT_TYPE_ID: { - const { groupBy } = request.body as MetricThresholdAlertPreviewRequestParams; + const { + groupBy, + criteria, + filterQuery, + } = request.body as MetricThresholdAlertPreviewRequestParams; const previewResult = await previewMetricThresholdAlert({ callCluster, params: { criteria, filterQuery, groupBy }, @@ -72,7 +78,11 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }); } case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: { - const { nodeType } = request.body as InventoryAlertPreviewRequestParams; + const { + nodeType, + criteria, + filterQuery, + } = request.body as InventoryAlertPreviewRequestParams; const previewResult = await previewInventoryMetricThresholdAlert({ callCluster, params: { criteria, filterQuery, nodeType }, @@ -89,6 +99,39 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) body: alertPreviewSuccessResponsePayloadRT.encode(payload), }); } + case METRIC_ANOMALY_ALERT_TYPE_ID: { + assertHasInfraMlPlugins(requestContext); + const { + nodeType, + metric, + threshold, + influencerFilter, + } = request.body as MetricAnomalyAlertPreviewRequestParams; + const { mlAnomalyDetectors, mlSystem, spaceId } = requestContext.infra; + + const previewResult = await previewMetricAnomalyAlert({ + mlAnomalyDetectors, + mlSystem, + spaceId, + params: { nodeType, metric, threshold, influencerFilter }, + lookback, + sourceId: source.id, + alertInterval, + alertThrottle, + alertOnNoData, + }); + + return response.ok({ + body: alertPreviewSuccessResponsePayloadRT.encode({ + numberOfGroups: 1, + resultTotals: { + ...previewResult, + error: 0, + noData: 0, + }, + }), + }); + } default: throw new Error('Unknown alert type'); } diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts index 8ec0b83994e1a..6e227cfc12d11 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts @@ -53,7 +53,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { hasMoreEntries, timing, } = await getMetricsHostsAnomalies( - requestContext, + requestContext.infra, sourceId, anomalyThreshold, startTime, diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts index d41fa0ffafecc..1c2c4947a02ea 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts @@ -52,7 +52,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => { hasMoreEntries, timing, } = await getMetricK8sAnomalies( - requestContext, + requestContext.infra, sourceId, anomalyThreshold, startTime, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6e9d0329eaff8..018d2d572eea0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9676,7 +9676,6 @@ "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", "xpack.infra.alerting.alertsButton": "アラート", - "xpack.infra.alerting.createAlertButton": "アラートの作成", "xpack.infra.alerting.logs.alertsButton": "アラート", "xpack.infra.alerting.logs.createAlertButton": "アラートの作成", "xpack.infra.alerting.logs.manageAlerts": "アラートを管理", @@ -9970,16 +9969,6 @@ "xpack.infra.logs.jumpToTailText": "最も新しいエントリーに移動", "xpack.infra.logs.lastUpdate": "前回の更新 {timestamp}", "xpack.infra.logs.loadingNewEntriesText": "新しいエントリーを読み込み中", - "xpack.infra.logs.logAnalysis.splash.learnMoreLink": "ドキュメンテーションを表示", - "xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "詳細について", - "xpack.infra.logs.logAnalysis.splash.loadingMessage": "ライセンスを確認しています...", - "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "プレースホルダー画像", - "xpack.infra.logs.logAnalysis.splash.startTrialCta": "トライアルを開始", - "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。", - "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "異常検知を利用するには、無料の試用版を開始してください", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "サブスクリプションのアップグレード", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "機械学習機能を使用するには、プラチナサブスクリプションが必要です。", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "異常検知を利用するには、プラチナサブスクリプションにアップグレードしてください", "xpack.infra.logs.logEntryActionsDetailsButton": "詳細を表示", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index eeda709104479..5a9695b8ddc3d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9702,7 +9702,6 @@ "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", "xpack.infra.alerting.alertsButton": "告警", - "xpack.infra.alerting.createAlertButton": "创建告警", "xpack.infra.alerting.logs.alertsButton": "告警", "xpack.infra.alerting.logs.createAlertButton": "创建告警", "xpack.infra.alerting.logs.manageAlerts": "管理告警", @@ -9997,16 +9996,6 @@ "xpack.infra.logs.jumpToTailText": "跳到最近的条目", "xpack.infra.logs.lastUpdate": "上次更新时间 {timestamp}", "xpack.infra.logs.loadingNewEntriesText": "正在加载新条目", - "xpack.infra.logs.logAnalysis.splash.learnMoreLink": "阅读文档", - "xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "希望了解详情?", - "xpack.infra.logs.logAnalysis.splash.loadingMessage": "正在检查许可证......", - "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "占位符图像", - "xpack.infra.logs.logAnalysis.splash.startTrialCta": "开始试用", - "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "我们的免费试用版包含 Machine Learning 功能,可用于检测日志中的异常。", - "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "要访问异常检测,请启动免费试用版", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "升级订阅", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "必须具有白金级订阅,才能使用 Machine Learning 功能。", - "xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "要访问异常检测,请升级到白金级订阅", "xpack.infra.logs.logEntryActionsDetailsButton": "查看详情", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析", "xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。", From ab401c98572780168ac589e1685b256004abe92e Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Feb 2021 23:29:52 -0800 Subject: [PATCH 093/100] skip flaky suite (#90136) --- x-pack/test/api_integration/apis/security_solution/users.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/api_integration/apis/security_solution/users.ts b/x-pack/test/api_integration/apis/security_solution/users.ts index 45e06ab72adbb..b888be2bf6276 100644 --- a/x-pack/test/api_integration/apis/security_solution/users.ts +++ b/x-pack/test/api_integration/apis/security_solution/users.ts @@ -23,6 +23,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); // Failing: See https://github.com/elastic/kibana/issues/90135 + // Failing: See https://github.com/elastic/kibana/issues/90136 describe.skip('Users', () => { describe('With auditbeat', () => { before(() => esArchiver.load('auditbeat/default')); From af277b83962d4bef12094bbbc17bad805a5c02a1 Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Wed, 10 Feb 2021 09:16:19 +0100 Subject: [PATCH 094/100] Add deprecation warning to all Beats CM pages. (#90741) --- .../beats_management/public/application.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/x-pack/plugins/beats_management/public/application.tsx b/x-pack/plugins/beats_management/public/application.tsx index 6e81809b9c493..5a9b0a768856e 100644 --- a/x-pack/plugins/beats_management/public/application.tsx +++ b/x-pack/plugins/beats_management/public/application.tsx @@ -7,11 +7,13 @@ import * as euiVars from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; import { Provider as UnstatedProvider, Subscribe } from 'unstated'; +import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; import { Background } from './components/layouts/background'; import { BreadcrumbProvider } from './components/navigation/breadcrumb'; import { Breadcrumb } from './components/navigation/breadcrumb/breadcrumb'; @@ -37,6 +39,38 @@ export const renderApp = ({ element, history }: ManagementAppMountParams, libs: defaultMessage: 'Management', })} /> + +

    + + + + ), + }} + /> +

    +
    + )} From 240da2bf2a999501eddc378466e67ebb8e26df76 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Wed, 10 Feb 2021 00:38:37 -0800 Subject: [PATCH 095/100] Actually deleting x-pack/tsconfig.refs.json (#90898) --- x-pack/tsconfig.refs.json | 60 --------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 x-pack/tsconfig.refs.json diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json deleted file mode 100644 index a36f4e205ab7d..0000000000000 --- a/x-pack/tsconfig.refs.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "include": [], - "references": [ - { "path": "./plugins/actions/tsconfig.json" }, - { "path": "./plugins/alerts/tsconfig.json" }, - { "path": "./plugins/beats_management/tsconfig.json" }, - { "path": "./plugins/canvas/tsconfig.json" }, - { "path": "./plugins/cloud/tsconfig.json" }, - { "path": "./plugins/code/tsconfig.json" }, - { "path": "./plugins/console_extensions/tsconfig.json" }, - { "path": "./plugins/dashboard_enhanced/tsconfig.json" }, - { "path": "./plugins/data_enhanced/tsconfig.json" }, - { "path": "./plugins/dashboard_mode/tsconfig.json" }, - { "path": "./plugins/discover_enhanced/tsconfig.json" }, - { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, - { "path": "./plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "./plugins/enterprise_search/tsconfig.json" }, - { "path": "./plugins/event_log/tsconfig.json" }, - { "path": "./plugins/features/tsconfig.json" }, - { "path": "./plugins/file_upload/tsconfig.json" }, - { "path": "./plugins/fleet/tsconfig.json" }, - { "path": "./plugins/global_search_bar/tsconfig.json" }, - { "path": "./plugins/global_search_providers/tsconfig.json" }, - { "path": "./plugins/global_search/tsconfig.json" }, - { "path": "./plugins/graph/tsconfig.json" }, - { "path": "./plugins/grokdebugger/tsconfig.json" }, - { "path": "./plugins/infra/tsconfig.json" }, - { "path": "./plugins/ingest_pipelines/tsconfig.json" }, - { "path": "./plugins/lens/tsconfig.json" }, - { "path": "./plugins/license_management/tsconfig.json" }, - { "path": "./plugins/licensing/tsconfig.json" }, - { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, - { "path": "./plugins/maps/tsconfig.json" }, - { "path": "./plugins/ml/tsconfig.json" }, - { "path": "./plugins/observability/tsconfig.json" }, - { "path": "./plugins/painless_lab/tsconfig.json" }, - { "path": "./plugins/reporting/tsconfig.json" }, - { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "./plugins/searchprofiler/tsconfig.json" }, - { "path": "./plugins/security/tsconfig.json" }, - { "path": "./plugins/snapshot_restore/tsconfig.json" }, - { "path": "./plugins/spaces/tsconfig.json" }, - { "path": "./plugins/stack_alerts/tsconfig.json" }, - { "path": "./plugins/task_manager/tsconfig.json" }, - { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, - { "path": "./plugins/transform/tsconfig.json" }, - { "path": "./plugins/translations/tsconfig.json" }, - { "path": "./plugins/triggers_actions_ui/tsconfig.json" }, - { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "./plugins/upgrade_assistant/tsconfig.json" }, - { "path": "./plugins/runtime_fields/tsconfig.json" }, - { "path": "./plugins/index_management/tsconfig.json" }, - { "path": "./plugins/watcher/tsconfig.json" }, - { "path": "./plugins/rollup/tsconfig.json"}, - { "path": "./plugins/remote_clusters/tsconfig.json"}, - { "path": "./plugins/cross_cluster_replication/tsconfig.json"}, - { "path": "./plugins/index_lifecycle_management/tsconfig.json"}, - { "path": "./plugins/uptime/tsconfig.json" } - ] -} From 634c0b34242adf87c78b72ed73051b0fd1c41014 Mon Sep 17 00:00:00 2001 From: Nicolas Ruflin Date: Wed, 10 Feb 2021 09:57:09 +0100 Subject: [PATCH 096/100] [Fleet] Use staging registry for snapshot builds (#90327) The staging registry is used in Kibana builds which are not built of the master branch or release version. This means, any build ending with `-SNAPSHOT` not the master branch will use the staging registry. Closes https://github.com/elastic/kibana/issues/90131 Co-authored-by: Jen Huang --- .../fleet/server/services/epm/registry/registry_url.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts index 1394d2738482d..8c637006fb0cd 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts @@ -12,7 +12,7 @@ import { appContextService, licenseService } from '../../'; // chose to comment them out vs @ts-ignore or @ts-expect-error on each line const PRODUCTION_REGISTRY_URL_CDN = 'https://epr.elastic.co'; -// const STAGING_REGISTRY_URL_CDN = 'https://epr-staging.elastic.co'; +const STAGING_REGISTRY_URL_CDN = 'https://epr-staging.elastic.co'; const SNAPSHOT_REGISTRY_URL_CDN = 'https://epr-snapshot.elastic.co'; // const PRODUCTION_REGISTRY_URL_NO_CDN = 'https://epr.ea-web.elastic.dev'; @@ -23,6 +23,8 @@ const getDefaultRegistryUrl = (): string => { const branch = appContextService.getKibanaBranch(); if (branch === 'master') { return SNAPSHOT_REGISTRY_URL_CDN; + } else if (appContextService.getKibanaVersion().includes('-SNAPSHOT')) { + return STAGING_REGISTRY_URL_CDN; } else { return PRODUCTION_REGISTRY_URL_CDN; } From 03a53b9f39a54a6accb8f37eb9fe84c882681511 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Wed, 10 Feb 2021 11:27:31 +0100 Subject: [PATCH 097/100] Do not generate an ephemeral encryption key in production. (#81511) --- .../server/action_type_registry.test.ts | 4 +- .../actions/server/actions_client.test.ts | 8 +- .../server/builtin_action_types/index.test.ts | 4 +- .../server/create_execute_function.test.ts | 14 +- .../actions/server/create_execute_function.ts | 8 +- .../server/lib/action_executor.test.ts | 8 +- .../actions/server/lib/action_executor.ts | 10 +- .../server/lib/task_runner_factory.test.ts | 8 +- x-pack/plugins/actions/server/plugin.test.ts | 44 +-- x-pack/plugins/actions/server/plugin.ts | 27 +- x-pack/plugins/alerts/server/plugin.test.ts | 18 +- x-pack/plugins/alerts/server/plugin.ts | 15 +- .../alerts/server/routes/health.test.ts | 26 +- x-pack/plugins/alerts/server/routes/health.ts | 2 +- .../server/config.test.ts | 68 ++-- .../encrypted_saved_objects/server/config.ts | 24 +- .../encrypted_saved_objects_service.test.ts | 334 ++++++++++++++++++ .../crypto/encrypted_saved_objects_service.ts | 53 ++- .../encrypted_saved_objects/server/mocks.ts | 6 +- .../server/plugin.test.ts | 28 +- .../encrypted_saved_objects/server/plugin.ts | 37 +- .../server/routes/index.mock.ts | 4 +- x-pack/plugins/fleet/kibana.json | 5 +- x-pack/plugins/fleet/server/plugin.ts | 8 +- .../fleet/server/routes/setup/handlers.ts | 4 +- .../elasticsearch/verify_alerting_security.ts | 2 +- .../privileges/read_privileges_route.test.ts | 2 +- .../privileges/read_privileges_route.ts | 4 +- .../security_solution/server/plugin.ts | 2 +- .../security_solution/server/routes/index.ts | 4 +- 30 files changed, 543 insertions(+), 238 deletions(-) diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index 813e47c2e9957..c8972d8113f16 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -26,9 +26,7 @@ beforeEach(() => { actionTypeRegistryParams = { licensing: licensingMock.createSetup(), taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) - ), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })), actionsConfigUtils: mockedActionsConfig, licenseState: mockedLicenseState, preconfiguredActions: [ diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 1bea3e1fc356d..3bd8bb5f1ba52 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -59,9 +59,7 @@ beforeEach(() => { actionTypeRegistryParams = { licensing: licensingMock.createSetup(), taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) - ), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })), actionsConfigUtils: actionsConfigMock.create(), licenseState: mockedLicenseState, preconfiguredActions: [], @@ -411,9 +409,7 @@ describe('create()', () => { const localActionTypeRegistryParams = { licensing: licensingMock.createSetup(), taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) - ), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })), actionsConfigUtils: localConfigUtils, licenseState: licenseStateMock.create(), preconfiguredActions: [], diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index bad709247d080..10955af2f3b13 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -33,9 +33,7 @@ export function createActionTypeRegistry(): { const actionTypeRegistry = new ActionTypeRegistry({ taskManager: taskManagerMock.createSetup(), licensing: licensingMock.createSetup(), - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) - ), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })), actionsConfigUtils: actionsConfigMock.create(), licenseState: licenseStateMock.create(), preconfiguredActions: [], diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index aaf11669c1d03..d4100537fa6b8 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -28,7 +28,7 @@ describe('execute()', () => { const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, actionTypeRegistry, - isESOUsingEphemeralEncryptionKey: false, + isESOCanEncrypt: true, preconfiguredActions: [], }); savedObjectsClient.get.mockResolvedValueOnce({ @@ -87,7 +87,7 @@ describe('execute()', () => { const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, actionTypeRegistry: actionTypeRegistryMock.create(), - isESOUsingEphemeralEncryptionKey: false, + isESOCanEncrypt: true, preconfiguredActions: [ { id: '123', @@ -158,10 +158,10 @@ describe('execute()', () => { ); }); - test('throws when passing isESOUsingEphemeralEncryptionKey with true as a value', async () => { + test('throws when passing isESOCanEncrypt with false as a value', async () => { const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, - isESOUsingEphemeralEncryptionKey: true, + isESOCanEncrypt: false, actionTypeRegistry: actionTypeRegistryMock.create(), preconfiguredActions: [], }); @@ -173,7 +173,7 @@ describe('execute()', () => { apiKey: null, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` + `"Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); @@ -181,7 +181,7 @@ describe('execute()', () => { const mockedActionTypeRegistry = actionTypeRegistryMock.create(); const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, - isESOUsingEphemeralEncryptionKey: false, + isESOCanEncrypt: true, actionTypeRegistry: mockedActionTypeRegistry, preconfiguredActions: [], }); @@ -211,7 +211,7 @@ describe('execute()', () => { const mockedActionTypeRegistry = actionTypeRegistryMock.create(); const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, - isESOUsingEphemeralEncryptionKey: false, + isESOCanEncrypt: true, actionTypeRegistry: mockedActionTypeRegistry, preconfiguredActions: [ { diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 0d75c0b410e44..025b4d3107798 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -14,7 +14,7 @@ import { isSavedObjectExecutionSource } from './lib'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; - isESOUsingEphemeralEncryptionKey: boolean; + isESOCanEncrypt: boolean; actionTypeRegistry: ActionTypeRegistryContract; preconfiguredActions: PreConfiguredAction[]; } @@ -33,16 +33,16 @@ export type ExecutionEnqueuer = ( export function createExecutionEnqueuerFunction({ taskManager, actionTypeRegistry, - isESOUsingEphemeralEncryptionKey, + isESOCanEncrypt, preconfiguredActions, }: CreateExecuteFunctionOptions) { return async function execute( unsecuredSavedObjectsClient: SavedObjectsClientContract, { id, params, spaceId, source, apiKey }: ExecuteOptions ) { - if (isESOUsingEphemeralEncryptionKey === true) { + if (!isESOCanEncrypt) { throw new Error( - `Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` + `Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` ); } diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index e9b72f9bf0e4e..8ec94c4d4a552 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -17,7 +17,7 @@ import { ActionType } from '../types'; import { actionsMock, actionsClientMock } from '../mocks'; import { pick } from 'lodash'; -const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }); +const actionExecutor = new ActionExecutor({ isESOCanEncrypt: true }); const services = actionsMock.createServices(); const actionsClient = actionsClientMock.create(); @@ -310,8 +310,8 @@ test('should not throws an error if actionType is preconfigured', async () => { }); }); -test('throws an error when passing isESOUsingEphemeralEncryptionKey with value of true', async () => { - const customActionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: true }); +test('throws an error when passing isESOCanEncrypt with value of false', async () => { + const customActionExecutor = new ActionExecutor({ isESOCanEncrypt: false }); customActionExecutor.initialize({ logger: loggingSystemMock.create().get(), spaces: spacesMock, @@ -325,7 +325,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o await expect( customActionExecutor.execute(executeParams) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` + `"Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 7a54f88e2f27c..6deaa4d587904 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -48,10 +48,10 @@ export type ActionExecutorContract = PublicMethodsOf; export class ActionExecutor { private isInitialized = false; private actionExecutorContext?: ActionExecutorContext; - private readonly isESOUsingEphemeralEncryptionKey: boolean; + private readonly isESOCanEncrypt: boolean; - constructor({ isESOUsingEphemeralEncryptionKey }: { isESOUsingEphemeralEncryptionKey: boolean }) { - this.isESOUsingEphemeralEncryptionKey = isESOUsingEphemeralEncryptionKey; + constructor({ isESOCanEncrypt }: { isESOCanEncrypt: boolean }) { + this.isESOCanEncrypt = isESOCanEncrypt; } public initialize(actionExecutorContext: ActionExecutorContext) { @@ -72,9 +72,9 @@ export class ActionExecutor { throw new Error('ActionExecutor not initialized'); } - if (this.isESOUsingEphemeralEncryptionKey === true) { + if (!this.isESOCanEncrypt) { throw new Error( - `Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` + `Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` ); } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index e42fc363f328b..9e101f2ee76b0 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -84,18 +84,14 @@ beforeEach(() => { }); test(`throws an error if factory isn't initialized`, () => { - const factory = new TaskRunnerFactory( - new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) - ); + const factory = new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })); expect(() => factory.create({ taskInstance: mockedTaskInstance }) ).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory not initialized"`); }); test(`throws an error if factory is already initialized`, () => { - const factory = new TaskRunnerFactory( - new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) - ); + const factory = new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })); factory.initialize(taskRunnerFactoryInitializerParams); expect(() => factory.initialize(taskRunnerFactoryInitializerParams) diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 187cba9d3240c..0e916220ca946 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -51,25 +51,21 @@ describe('Actions Plugin', () => { }; }); - it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => { - // coreMock.createSetup doesn't support Plugin generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await plugin.setup(coreSetup as any, pluginsSetup); - expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); + it('should log warning when Encrypted Saved Objects plugin is missing encryption key', async () => { + await plugin.setup(coreSetup, pluginsSetup); + expect(pluginsSetup.encryptedSavedObjects.canEncrypt).toEqual(false); expect(context.logger.get().warn).toHaveBeenCalledWith( - 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' + 'APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); }); describe('routeHandlerContext.getActionsClient()', () => { - it('should not throw error when ESO plugin not using a generated key', async () => { - // coreMock.createSetup doesn't support Plugin generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await plugin.setup(coreSetup as any, { + it('should not throw error when ESO plugin has encryption key', async () => { + await plugin.setup(coreSetup, { ...pluginsSetup, encryptedSavedObjects: { ...pluginsSetup.encryptedSavedObjects, - usingEphemeralEncryptionKey: false, + canEncrypt: true, }, }); @@ -99,10 +95,8 @@ describe('Actions Plugin', () => { actionsContextHandler!.getActionsClient(); }); - it('should throw error when ESO plugin using a generated key', async () => { - // coreMock.createSetup doesn't support Plugin generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await plugin.setup(coreSetup as any, pluginsSetup); + it('should throw error when ESO plugin is missing encryption key', async () => { + await plugin.setup(coreSetup, pluginsSetup); expect(coreSetup.http.registerRouteHandlerContext).toHaveBeenCalledTimes(1); const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0] as [ @@ -123,7 +117,7 @@ describe('Actions Plugin', () => { httpServerMock.createResponseFactory() )) as unknown) as ActionsApiRequestHandlerContext; expect(() => actionsContextHandler!.getActionsClient()).toThrowErrorMatchingInlineSnapshot( - `"Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` + `"Unable to create actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); }); @@ -234,14 +228,12 @@ describe('Actions Plugin', () => { expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true); }); - it('should not throw error when ESO plugin not using a generated key', async () => { - // coreMock.createSetup doesn't support Plugin generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await plugin.setup(coreSetup as any, { + it('should not throw error when ESO plugin has encryption key', async () => { + await plugin.setup(coreSetup, { ...pluginsSetup, encryptedSavedObjects: { ...pluginsSetup.encryptedSavedObjects, - usingEphemeralEncryptionKey: false, + canEncrypt: true, }, }); const pluginStart = await plugin.start(coreStart, pluginsStart); @@ -249,17 +241,15 @@ describe('Actions Plugin', () => { await pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest()); }); - it('should throw error when ESO plugin using generated key', async () => { - // coreMock.createSetup doesn't support Plugin generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await plugin.setup(coreSetup as any, pluginsSetup); + it('should throw error when ESO plugin is missing encryption key', async () => { + await plugin.setup(coreSetup, pluginsSetup); const pluginStart = await plugin.start(coreStart, pluginsStart); - expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); + expect(pluginsSetup.encryptedSavedObjects.canEncrypt).toEqual(false); await expect( pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest()) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` + `"Unable to create actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); }); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 8fbacc71d30cb..c4159c80e806f 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -144,7 +144,7 @@ export class ActionsPlugin implements Plugin ) => { - if (isESOUsingEphemeralEncryptionKey === true) { + if (isESOCanEncrypt !== true) { throw new Error( - `Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` + `Unable to create actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` ); } @@ -314,7 +313,7 @@ export class ActionsPlugin implements Plugin => { const { actionTypeRegistry, - isESOUsingEphemeralEncryptionKey, + isESOCanEncrypt, preconfiguredActions, actionExecutor, instantiateAuthorization, @@ -448,9 +447,9 @@ export class ActionsPlugin implements Plugin { - if (isESOUsingEphemeralEncryptionKey === true) { + if (isESOCanEncrypt !== true) { throw new Error( - `Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` + `Unable to create actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` ); } return new ActionsClient({ @@ -468,7 +467,7 @@ export class ActionsPlugin implements Plugin { let coreSetup: ReturnType; let pluginsSetup: jest.Mocked; - it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => { + it('should log warning when Encrypted Saved Objects plugin is missing encryption key', async () => { const context = coreMock.createPluginInitializerContext({ healthCheck: { interval: '5m', @@ -40,7 +40,7 @@ describe('Alerting Plugin', () => { const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); const setupMocks = coreMock.createSetup(); - // need await to test number of calls of setupMocks.status.set, becuase it is under async function which awaiting core.getStartServices() + // need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices() await plugin.setup(setupMocks, { licensing: licensingMock.createSetup(), encryptedSavedObjects: encryptedSavedObjectsSetup, @@ -51,9 +51,9 @@ describe('Alerting Plugin', () => { }); expect(setupMocks.status.set).toHaveBeenCalledTimes(1); - expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true); + expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false); expect(context.logger.get().warn).toHaveBeenCalledWith( - 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' + 'APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); }); @@ -110,7 +110,7 @@ describe('Alerting Plugin', () => { describe('start()', () => { describe('getAlertsClientWithRequest()', () => { - it('throws error when encryptedSavedObjects plugin has usingEphemeralEncryptionKey set to true', async () => { + it('throws error when encryptedSavedObjects plugin is missing encryption key', async () => { const context = coreMock.createPluginInitializerContext({ healthCheck: { interval: '5m', @@ -141,15 +141,15 @@ describe('Alerting Plugin', () => { taskManager: taskManagerMock.createStart(), }); - expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true); + expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false); expect(() => startContract.getAlertsClientWithRequest({} as KibanaRequest) ).toThrowErrorMatchingInlineSnapshot( - `"Unable to create alerts client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` + `"Unable to create alerts client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."` ); }); - it(`doesn't throw error when encryptedSavedObjects plugin has usingEphemeralEncryptionKey set to false`, async () => { + it(`doesn't throw error when encryptedSavedObjects plugin has encryption key`, async () => { const context = coreMock.createPluginInitializerContext({ healthCheck: { interval: '5m', @@ -163,7 +163,7 @@ describe('Alerting Plugin', () => { const encryptedSavedObjectsSetup = { ...encryptedSavedObjectsMock.createSetup(), - usingEphemeralEncryptionKey: false, + canEncrypt: true, }; plugin.setup(coreMock.createSetup(), { licensing: licensingMock.createSetup(), diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index aaec0bb8a080d..8dba4453d5682 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -153,7 +153,7 @@ export class AlertingPlugin { private alertTypeRegistry?: AlertTypeRegistry; private readonly taskRunnerFactory: TaskRunnerFactory; private licenseState: ILicenseState | null = null; - private isESOUsingEphemeralEncryptionKey?: boolean; + private isESOCanEncrypt?: boolean; private security?: SecurityPluginSetup; private readonly alertsClientFactory: AlertsClientFactory; private readonly telemetryLogger: Logger; @@ -189,12 +189,11 @@ export class AlertingPlugin { }; }); - this.isESOUsingEphemeralEncryptionKey = - plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; + this.isESOCanEncrypt = plugins.encryptedSavedObjects.canEncrypt; - if (this.isESOUsingEphemeralEncryptionKey) { + if (!this.isESOCanEncrypt) { this.logger.warn( - 'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' + 'APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); } @@ -311,7 +310,7 @@ export class AlertingPlugin { public start(core: CoreStart, plugins: AlertingPluginsStart): PluginStartContract { const { - isESOUsingEphemeralEncryptionKey, + isESOCanEncrypt, logger, taskRunnerFactory, alertTypeRegistry, @@ -353,9 +352,9 @@ export class AlertingPlugin { }); const getAlertsClientWithRequest = (request: KibanaRequest) => { - if (isESOUsingEphemeralEncryptionKey === true) { + if (isESOCanEncrypt !== true) { throw new Error( - `Unable to create alerts client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` + `Unable to create alerts client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.` ); } return alertsClientFactory!.create(request, core.savedObjects); diff --git a/x-pack/plugins/alerts/server/routes/health.test.ts b/x-pack/plugins/alerts/server/routes/health.test.ts index 38bae896e40ba..22df0e6a00046 100644 --- a/x-pack/plugins/alerts/server/routes/health.test.ts +++ b/x-pack/plugins/alerts/server/routes/health.test.ts @@ -47,8 +47,7 @@ describe('healthRoute', () => { const router = httpServiceMock.createRouter(); const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); - encryptedSavedObjects.usingEphemeralEncryptionKey = false; + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); healthRoute(router, licenseState, encryptedSavedObjects); const [config] = router.get.mock.calls[0]; @@ -60,8 +59,7 @@ describe('healthRoute', () => { const router = httpServiceMock.createRouter(); const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); - encryptedSavedObjects.usingEphemeralEncryptionKey = false; + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; @@ -85,12 +83,11 @@ describe('healthRoute', () => { `); }); - it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => { + it('evaluates whether Encrypted Saved Objects is missing encryption key', async () => { const router = httpServiceMock.createRouter(); const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); - encryptedSavedObjects.usingEphemeralEncryptionKey = true; + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: false }); healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; @@ -129,8 +126,7 @@ describe('healthRoute', () => { const router = httpServiceMock.createRouter(); const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); - encryptedSavedObjects.usingEphemeralEncryptionKey = false; + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; @@ -169,8 +165,7 @@ describe('healthRoute', () => { const router = httpServiceMock.createRouter(); const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); - encryptedSavedObjects.usingEphemeralEncryptionKey = false; + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; @@ -209,8 +204,7 @@ describe('healthRoute', () => { const router = httpServiceMock.createRouter(); const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); - encryptedSavedObjects.usingEphemeralEncryptionKey = false; + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; @@ -249,8 +243,7 @@ describe('healthRoute', () => { const router = httpServiceMock.createRouter(); const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); - encryptedSavedObjects.usingEphemeralEncryptionKey = false; + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; @@ -291,8 +284,7 @@ describe('healthRoute', () => { const router = httpServiceMock.createRouter(); const licenseState = licenseStateMock.create(); - const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); - encryptedSavedObjects.usingEphemeralEncryptionKey = false; + const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true }); healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; diff --git a/x-pack/plugins/alerts/server/routes/health.ts b/x-pack/plugins/alerts/server/routes/health.ts index 24b3642ca2085..9e1f01041e091 100644 --- a/x-pack/plugins/alerts/server/routes/health.ts +++ b/x-pack/plugins/alerts/server/routes/health.ts @@ -55,7 +55,7 @@ export function healthRoute( const frameworkHealth: AlertingFrameworkHealth = { isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), - hasPermanentEncryptionKey: !encryptedSavedObjects.usingEphemeralEncryptionKey, + hasPermanentEncryptionKey: encryptedSavedObjects.canEncrypt, alertingFrameworkHeath, }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts index 3633dae824a2b..1cc5f7974cb13 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts @@ -5,10 +5,7 @@ * 2.0. */ -jest.mock('crypto', () => ({ randomBytes: jest.fn() })); - -import { loggingSystemMock } from 'src/core/server/mocks'; -import { createConfig, ConfigSchema } from './config'; +import { ConfigSchema } from './config'; describe('config schema', () => { it('generates proper defaults', () => { @@ -32,6 +29,17 @@ describe('config schema', () => { } `); + expect(ConfigSchema.validate({ encryptionKey: 'z'.repeat(32) }, { dist: true })) + .toMatchInlineSnapshot(` + Object { + "enabled": true, + "encryptionKey": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", + "keyRotation": Object { + "decryptionOnlyKeys": Array [], + }, + } + `); + expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(` Object { "enabled": true, @@ -79,6 +87,18 @@ describe('config schema', () => { ); }); + it('should not allow `null` value for the encryption key', () => { + expect(() => ConfigSchema.validate({ encryptionKey: null })).toThrowErrorMatchingInlineSnapshot( + `"[encryptionKey]: expected value of type [string] but got [null]"` + ); + + expect(() => + ConfigSchema.validate({ encryptionKey: null }, { dist: true }) + ).toThrowErrorMatchingInlineSnapshot( + `"[encryptionKey]: expected value of type [string] but got [null]"` + ); + }); + it('should throw error if any of the xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys is less than 32 characters', () => { expect(() => ConfigSchema.validate({ @@ -121,43 +141,3 @@ describe('config schema', () => { ); }); }); - -describe('createConfig()', () => { - it('should log a warning, set xpack.encryptedSavedObjects.encryptionKey and usingEphemeralEncryptionKey=true when encryptionKey is not set', () => { - const mockRandomBytes = jest.requireMock('crypto').randomBytes; - mockRandomBytes.mockReturnValue('ab'.repeat(16)); - - const logger = loggingSystemMock.create().get(); - const config = createConfig(ConfigSchema.validate({}, { dist: true }), logger); - expect(config).toEqual({ - enabled: true, - encryptionKey: 'ab'.repeat(16), - keyRotation: { decryptionOnlyKeys: [] }, - usingEphemeralEncryptionKey: true, - }); - - expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` - Array [ - Array [ - "Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.", - ], - ] - `); - }); - - it('should not log a warning and set usingEphemeralEncryptionKey=false when encryptionKey is set', async () => { - const logger = loggingSystemMock.create().get(); - const config = createConfig( - ConfigSchema.validate({ encryptionKey: 'supersecret'.repeat(3) }, { dist: true }), - logger - ); - expect(config).toEqual({ - enabled: true, - encryptionKey: 'supersecret'.repeat(3), - keyRotation: { decryptionOnlyKeys: [] }, - usingEphemeralEncryptionKey: false, - }); - - expect(loggingSystemMock.collect(logger).warn).toEqual([]); - }); -}); diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.ts b/x-pack/plugins/encrypted_saved_objects/server/config.ts index 40db0187162d0..2bcf0e9b69511 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.ts @@ -5,11 +5,9 @@ * 2.0. */ -import crypto from 'crypto'; import { schema, TypeOf } from '@kbn/config-schema'; -import { Logger } from 'src/core/server'; -export type ConfigType = ReturnType; +export type ConfigType = TypeOf; export const ConfigSchema = schema.object( { @@ -33,23 +31,3 @@ export const ConfigSchema = schema.object( }, } ); - -export function createConfig(config: TypeOf, logger: Logger) { - let encryptionKey = config.encryptionKey; - const usingEphemeralEncryptionKey = encryptionKey === undefined; - if (encryptionKey === undefined) { - logger.warn( - 'Generating a random key for xpack.encryptedSavedObjects.encryptionKey. ' + - 'To decrypt encrypted saved objects attributes after restart, ' + - 'please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' - ); - - encryptionKey = crypto.randomBytes(16).toString('hex'); - } - - return { - ...config, - encryptionKey, - usingEphemeralEncryptionKey, - }; -} diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts index 1760a85806786..f70810943d179 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts @@ -226,6 +226,72 @@ describe('#stripOrDecryptAttributes', () => { ); }); }); + + describe('without encryption key', () => { + beforeEach(() => { + service = new EncryptedSavedObjectsService({ + logger: loggingSystemMock.create().get(), + audit: mockAuditLogger, + }); + }); + + it('does not fail if none of attributes are supposed to be encrypted', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) }); + + await expect( + service.stripOrDecryptAttributes({ id: 'known-id', type: 'known-type-1' }, attributes) + ).resolves.toEqual({ attributes: { attrOne: 'one', attrTwo: 'two', attrThree: 'three' } }); + }); + + it('does not fail if there are attributes are supposed to be encrypted, but should be stripped', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + await expect( + service.stripOrDecryptAttributes({ id: 'known-id', type: 'known-type-1' }, attributes) + ).resolves.toEqual({ attributes: { attrTwo: 'two' } }); + }); + + it('fails if needs to decrypt any attribute', async () => { + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set([ + 'attrOne', + { key: 'attrThree', dangerouslyExposeValue: true }, + ]), + }); + + const mockUser = mockAuthenticatedUser(); + const { attributes, error } = await service.stripOrDecryptAttributes( + { type: 'known-type-1', id: 'object-id' }, + { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }, + undefined, + { user: mockUser } + ); + + expect(attributes).toEqual({ attrTwo: 'two' }); + + const encryptionError = error as EncryptionError; + expect(encryptionError.attributeName).toBe('attrThree'); + expect(encryptionError.message).toBe('Unable to decrypt attribute "attrThree"'); + expect(encryptionError.cause).toEqual( + new Error('Decryption is disabled because of missing decryption keys.') + ); + + expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith( + 'attrThree', + { type: 'known-type-1', id: 'object-id' }, + mockUser + ); + }); + }); }); describe('#encryptAttributes', () => { @@ -465,6 +531,58 @@ describe('#encryptAttributes', () => { mockUser ); }); + + describe('without encryption key', () => { + beforeEach(() => { + service = new EncryptedSavedObjectsService({ + logger: loggingSystemMock.create().get(), + audit: mockAuditLogger, + }); + }); + + it('does not fail if none of attributes are supposed to be encrypted', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) }); + + await expect( + service.encryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes) + ).resolves.toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockAuditLogger.encryptAttributesSuccess).not.toHaveBeenCalled(); + }); + + it('fails if needs to encrypt any attribute', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + const mockUser = mockAuthenticatedUser(); + await expect( + service.encryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes, { + user: mockUser, + }) + ).rejects.toThrowError(EncryptionError); + + expect(attributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockAuditLogger.encryptAttributesSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledWith( + 'attrOne', + { type: 'known-type-1', id: 'object-id' }, + mockUser + ); + }); + }); }); describe('#decryptAttributes', () => { @@ -1099,6 +1217,88 @@ describe('#decryptAttributes', () => { expect(decryptionOnlyCryptoTwo.decrypt).not.toHaveBeenCalled(); }); }); + + describe('without encryption key', () => { + beforeEach(() => { + service = new EncryptedSavedObjectsService({ + logger: loggingSystemMock.create().get(), + audit: mockAuditLogger, + }); + }); + + it('does not fail if none of attributes are supposed to be decrypted', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service = new EncryptedSavedObjectsService({ + decryptionOnlyCryptos: [], + logger: loggingSystemMock.create().get(), + audit: mockAuditLogger, + }); + service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) }); + + await expect( + service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes) + ).resolves.toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled(); + }); + + it('does not fail if can decrypt attributes with decryption only keys', async () => { + const decryptionOnlyCryptoOne = createNodeCryptMock('old-key-one'); + decryptionOnlyCryptoOne.decrypt.mockImplementation( + async (encryptedOutput: string | Buffer, aad?: string) => `${encryptedOutput}||${aad}` + ); + + service = new EncryptedSavedObjectsService({ + decryptionOnlyCryptos: [decryptionOnlyCryptoOne], + logger: loggingSystemMock.create().get(), + audit: mockAuditLogger, + }); + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour']), + }); + + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three', attrFour: null }; + await expect( + service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes) + ).resolves.toEqual({ + attrOne: 'one||["known-type-1","object-id",{"attrTwo":"two"}]', + attrTwo: 'two', + attrThree: 'three||["known-type-1","object-id",{"attrTwo":"two"}]', + attrFour: null, + }); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrOne', 'attrThree'], + { type: 'known-type-1', id: 'object-id' }, + undefined + ); + }); + + it('fails if needs to decrypt any attribute', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrOne']) }); + + const mockUser = mockAuthenticatedUser(); + await expect( + service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes, { + user: mockUser, + }) + ).rejects.toThrowError(EncryptionError); + + expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith( + 'attrOne', + { type: 'known-type-1', id: 'object-id' }, + mockUser + ); + }); + }); }); describe('#encryptAttributesSync', () => { @@ -1283,6 +1483,58 @@ describe('#encryptAttributesSync', () => { attrThree: 'three', }); }); + + describe('without encryption key', () => { + beforeEach(() => { + service = new EncryptedSavedObjectsService({ + logger: loggingSystemMock.create().get(), + audit: mockAuditLogger, + }); + }); + + it('does not fail if none of attributes are supposed to be encrypted', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) }); + + expect( + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockAuditLogger.encryptAttributesSuccess).not.toHaveBeenCalled(); + }); + + it('fails if needs to encrypt any attribute', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + const mockUser = mockAuthenticatedUser(); + expect(() => + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes, { + user: mockUser, + }) + ).toThrowError(EncryptionError); + + expect(attributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockAuditLogger.encryptAttributesSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledWith( + 'attrOne', + { type: 'known-type-1', id: 'object-id' }, + mockUser + ); + }); + }); }); describe('#decryptAttributesSync', () => { @@ -1784,4 +2036,86 @@ describe('#decryptAttributesSync', () => { expect(decryptionOnlyCryptoTwo.decryptSync).not.toHaveBeenCalled(); }); }); + + describe('without encryption key', () => { + beforeEach(() => { + service = new EncryptedSavedObjectsService({ + logger: loggingSystemMock.create().get(), + audit: mockAuditLogger, + }); + }); + + it('does not fail if none of attributes are supposed to be decrypted', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service = new EncryptedSavedObjectsService({ + decryptionOnlyCryptos: [], + logger: loggingSystemMock.create().get(), + audit: mockAuditLogger, + }); + service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled(); + }); + + it('does not fail if can decrypt attributes with decryption only keys', () => { + const decryptionOnlyCryptoOne = createNodeCryptMock('old-key-one'); + decryptionOnlyCryptoOne.decryptSync.mockImplementation( + (encryptedOutput: string | Buffer, aad?: string) => `${encryptedOutput}||${aad}` + ); + + service = new EncryptedSavedObjectsService({ + decryptionOnlyCryptos: [decryptionOnlyCryptoOne], + logger: loggingSystemMock.create().get(), + audit: mockAuditLogger, + }); + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour']), + }); + + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three', attrFour: null }; + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrOne: 'one||["known-type-1","object-id",{"attrTwo":"two"}]', + attrTwo: 'two', + attrThree: 'three||["known-type-1","object-id",{"attrTwo":"two"}]', + attrFour: null, + }); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrOne', 'attrThree'], + { type: 'known-type-1', id: 'object-id' }, + undefined + ); + }); + + it('fails if needs to decrypt any attribute', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrOne']) }); + + const mockUser = mockAuthenticatedUser(); + expect(() => + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes, { + user: mockUser, + }) + ).toThrowError(EncryptionError); + + expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith( + 'attrOne', + { type: 'known-type-1', id: 'object-id' }, + mockUser + ); + }); + }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts index 91a3cfc921624..23aef07ff8781 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts @@ -77,7 +77,7 @@ interface EncryptedSavedObjectsServiceOptions { /** * NodeCrypto instance used for both encryption and decryption. */ - primaryCrypto: Crypto; + primaryCrypto?: Crypto; /** * NodeCrypto instances used ONLY for decryption (i.e. rotated encryption keys). @@ -293,12 +293,17 @@ export class EncryptedSavedObjectsService { let iteratorResult = iterator.next(); while (!iteratorResult.done) { const [attributeValue, encryptionAAD] = iteratorResult.value; - try { - iteratorResult = iterator.next( - await this.options.primaryCrypto.encrypt(attributeValue, encryptionAAD) - ); - } catch (err) { - iterator.throw!(err); + // We check this inside of the iterator to throw only if we do need to encrypt anything. + if (this.options.primaryCrypto) { + try { + iteratorResult = iterator.next( + await this.options.primaryCrypto.encrypt(attributeValue, encryptionAAD) + ); + } catch (err) { + iterator.throw!(err); + } + } else { + iterator.throw!(new Error('Encryption is disabled because of missing encryption key.')); } } @@ -324,12 +329,17 @@ export class EncryptedSavedObjectsService { let iteratorResult = iterator.next(); while (!iteratorResult.done) { const [attributeValue, encryptionAAD] = iteratorResult.value; - try { - iteratorResult = iterator.next( - this.options.primaryCrypto.encryptSync(attributeValue, encryptionAAD) - ); - } catch (err) { - iterator.throw!(err); + // We check this inside of the iterator to throw only if we do need to encrypt anything. + if (this.options.primaryCrypto) { + try { + iteratorResult = iterator.next( + this.options.primaryCrypto.encryptSync(attributeValue, encryptionAAD) + ); + } catch (err) { + iterator.throw!(err); + } + } else { + iterator.throw!(new Error('Encryption is disabled because of missing encryption key.')); } } @@ -358,7 +368,11 @@ export class EncryptedSavedObjectsService { while (!iteratorResult.done) { const [attributeValue, encryptionAAD] = iteratorResult.value; - let decryptionError; + // We check this inside of the iterator to throw only if we do need to decrypt anything. + let decryptionError = + decrypters.length === 0 + ? new Error('Decryption is disabled because of missing decryption keys.') + : undefined; for (const decrypter of decrypters) { try { iteratorResult = iterator.next(await decrypter.decrypt(attributeValue, encryptionAAD)); @@ -402,7 +416,11 @@ export class EncryptedSavedObjectsService { while (!iteratorResult.done) { const [attributeValue, encryptionAAD] = iteratorResult.value; - let decryptionError; + // We check this inside of the iterator to throw only if we do need to decrypt anything. + let decryptionError = + decrypters.length === 0 + ? new Error('Decryption is disabled because of missing decryption keys.') + : undefined; for (const decrypter of decrypters) { try { iteratorResult = iterator.next(decrypter.decryptSync(attributeValue, encryptionAAD)); @@ -541,6 +559,9 @@ export class EncryptedSavedObjectsService { return this.options.decryptionOnlyCryptos; } - return [this.options.primaryCrypto, ...(this.options.decryptionOnlyCryptos ?? [])]; + return [ + ...(this.options.primaryCrypto ? [this.options.primaryCrypto] : []), + ...(this.options.decryptionOnlyCryptos ?? []), + ]; } } diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 6c8196b2ae03c..edb55513aabf5 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -8,11 +8,13 @@ import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; import { EncryptedSavedObjectsClient, EncryptedSavedObjectsClientOptions } from './saved_objects'; -function createEncryptedSavedObjectsSetupMock() { +function createEncryptedSavedObjectsSetupMock( + { canEncrypt }: { canEncrypt: boolean } = { canEncrypt: false } +) { return { registerType: jest.fn(), __legacyCompat: { registerLegacyAPI: jest.fn() }, - usingEphemeralEncryptionKey: true, + canEncrypt, createMigration: jest.fn(), } as jest.Mocked; } diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 823a6b0afa9dc..e71332b1c5aa7 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -19,12 +19,28 @@ describe('EncryptedSavedObjects Plugin', () => { ); expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) .toMatchInlineSnapshot(` - Object { - "createMigration": [Function], - "registerType": [Function], - "usingEphemeralEncryptionKey": true, - } - `); + Object { + "canEncrypt": false, + "createMigration": [Function], + "registerType": [Function], + } + `); + }); + + it('exposes proper contract when encryption key is set', () => { + const plugin = new EncryptedSavedObjectsPlugin( + coreMock.createPluginInitializerContext( + ConfigSchema.validate({ encryptionKey: 'z'.repeat(32) }, { dist: true }) + ) + ); + expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) + .toMatchInlineSnapshot(` + Object { + "canEncrypt": true, + "createMigration": [Function], + "registerType": [Function], + } + `); }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index e846b133c26e0..c99d6bd32287d 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -6,10 +6,9 @@ */ import nodeCrypto from '@elastic/node-crypto'; -import { Logger, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; -import { TypeOf } from '@kbn/config-schema'; -import { SecurityPluginSetup } from '../../security/server'; -import { createConfig, ConfigSchema } from './config'; +import type { Logger, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; +import type { SecurityPluginSetup } from '../../security/server'; +import type { ConfigType } from './config'; import { EncryptedSavedObjectsService, EncryptedSavedObjectTypeRegistration, @@ -26,8 +25,11 @@ export interface PluginsSetup { } export interface EncryptedSavedObjectsPluginSetup { + /** + * Indicates if Saved Object encryption is possible. Requires an encryption key to be explicitly set via `xpack.encryptedSavedObjects.encryptionKey`. + */ + canEncrypt: boolean; registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; - usingEphemeralEncryptionKey: boolean; createMigration: CreateEncryptedSavedObjectsMigrationFn; } @@ -50,19 +52,24 @@ export class EncryptedSavedObjectsPlugin } public setup(core: CoreSetup, deps: PluginsSetup): EncryptedSavedObjectsPluginSetup { - const config = createConfig( - this.initializerContext.config.get>(), - this.initializerContext.logger.get('config') - ); - const auditLogger = new EncryptedSavedObjectsAuditLogger( - deps.security?.audit.getLogger('encryptedSavedObjects') - ); + const config = this.initializerContext.config.get(); + const canEncrypt = config.encryptionKey !== undefined; + if (!canEncrypt) { + this.logger.warn( + 'Saved objects encryption key is not set. This will severely limit Kibana functionality. ' + + 'Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' + ); + } - const primaryCrypto = nodeCrypto({ encryptionKey: config.encryptionKey }); + const primaryCrypto = config.encryptionKey + ? nodeCrypto({ encryptionKey: config.encryptionKey }) + : undefined; const decryptionOnlyCryptos = config.keyRotation.decryptionOnlyKeys.map((decryptionKey) => nodeCrypto({ encryptionKey: decryptionKey }) ); - + const auditLogger = new EncryptedSavedObjectsAuditLogger( + deps.security?.audit.getLogger('encryptedSavedObjects') + ); const service = Object.freeze( new EncryptedSavedObjectsService({ primaryCrypto, @@ -94,9 +101,9 @@ export class EncryptedSavedObjectsPlugin }); return { + canEncrypt, registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => service.registerType(typeRegistration), - usingEphemeralEncryptionKey: config.usingEphemeralEncryptionKey, createMigration: getCreateMigration( service, (typeRegistration: EncryptedSavedObjectTypeRegistration) => { diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts index c2dbc4c163b44..32ac1617f4a7e 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConfigSchema, createConfig } from '../config'; +import { ConfigSchema, ConfigType } from '../config'; import { httpServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; import { encryptionKeyRotationServiceMock } from '../crypto/index.mock'; @@ -14,7 +14,7 @@ export const routeDefinitionParamsMock = { create: (config: Record = {}) => ({ router: httpServiceMock.createRouter(), logger: loggingSystemMock.create().get(), - config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get()), + config: ConfigSchema.validate(config) as ConfigType, encryptionKeyRotationService: encryptionKeyRotationServiceMock.create(), }), }; diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json index aa0761c8a39bd..4a4019e3e9e47 100644 --- a/x-pack/plugins/fleet/kibana.json +++ b/x-pack/plugins/fleet/kibana.json @@ -4,14 +4,13 @@ "server": true, "ui": true, "configPath": ["xpack", "fleet"], - "requiredPlugins": ["licensing", "data"], + "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], "optionalPlugins": [ "security", "features", "cloud", "usageCollection", - "home", - "encryptedSavedObjects" + "home" ], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaReact", "esUiShared", "home", "infra", "kibanaUtils"] diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 7378d45e1bb3a..d89db7f1ac341 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -95,7 +95,7 @@ export interface FleetSetupDeps { } export interface FleetStartDeps { - encryptedSavedObjects?: EncryptedSavedObjectsPluginStart; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; security?: SecurityPluginStart; } @@ -255,11 +255,11 @@ export class FleetPlugin // Conditional config routes if (config.agents.enabled) { - const isESOUsingEphemeralEncryptionKey = !deps.encryptedSavedObjects; - if (isESOUsingEphemeralEncryptionKey) { + const isESOCanEncrypt = deps.encryptedSavedObjects.canEncrypt; + if (!isESOCanEncrypt) { if (this.logger) { this.logger.warn( - 'Fleet APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' + 'Fleet APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.' ); } } else { diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index 1e74469107db4..0c6ba6d14b1be 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -24,7 +24,7 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re const isProductionMode = appContextService.getIsProductionMode(); const isCloud = appContextService.getCloud()?.isCloudEnabled ?? false; const isTLSCheckDisabled = appContextService.getConfig()?.agents?.tlsCheckDisabled ?? false; - const isUsingEphemeralEncryptionKey = !appContextService.getEncryptedSavedObjectsSetup(); + const canEncrypt = appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt === true; const missingRequirements: GetFleetStatusResponse['missing_requirements'] = []; if (!isAdminUserSetup) { @@ -37,7 +37,7 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re missingRequirements.push('tls_required'); } - if (isUsingEphemeralEncryptionKey) { + if (!canEncrypt) { missingRequirements.push('encrypted_saved_object_encryption_key_required'); } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts index c81b9632f0cd7..facb6e29236e3 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts @@ -44,7 +44,7 @@ export class AlertingSecurity { return { isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), - hasPermanentEncryptionKey: Boolean(encryptedSavedObjects), + hasPermanentEncryptionKey: encryptedSavedObjects?.canEncrypt === true, }; }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index 3ee3c6884a3ec..2efb65c4a49a2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -18,7 +18,7 @@ describe('read_privileges route', () => { ({ clients, context } = requestContextMock.createTools()); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivilegesResult()); - readPrivilegesRoute(server.router, false); + readPrivilegesRoute(server.router, true); }); describe('normal status codes', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index a934f0a0ce134..f006d9250d369 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -14,7 +14,7 @@ import { readPrivileges } from '../../privileges/read_privileges'; export const readPrivilegesRoute = ( router: SecuritySolutionPluginRouter, - usingEphemeralEncryptionKey: boolean + hasEncryptionKey: boolean ) => { router.get( { @@ -39,7 +39,7 @@ export const readPrivilegesRoute = ( const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index); const privileges = merge(clusterPrivileges, { is_authenticated: request.auth.isAuthenticated ?? false, - has_encryption_key: !usingEphemeralEncryptionKey, + has_encryption_key: hasEncryptionKey, }); return response.ok({ body: privileges }); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 8c35fd2ce8f8b..a34193937c788 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -183,7 +183,7 @@ export class Plugin implements IPlugin { @@ -102,5 +102,5 @@ export const initRoutes = ( readTagsRoute(router); // Privileges API to get the generic user privileges - readPrivilegesRoute(router, usingEphemeralEncryptionKey); + readPrivilegesRoute(router, hasEncryptionKey); }; From 4ee960380152b825862fa621e8b30474818cd9bd Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Wed, 10 Feb 2021 11:33:17 +0100 Subject: [PATCH 098/100] Use new shortcut links to Fleet discuss forums. (#90786) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../fleet/public/applications/fleet/components/alpha_flyout.tsx | 2 +- .../plugins/fleet/public/applications/fleet/layouts/default.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/alpha_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/alpha_flyout.tsx index 82b2d20005225..c91d80124dd35 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/alpha_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/alpha_flyout.tsx @@ -61,7 +61,7 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { ), forumLink: ( - + = ({ Date: Wed, 10 Feb 2021 13:50:41 +0100 Subject: [PATCH 099/100] [Lens] (Accessibility) Fix focus on drag and drop actions (#90561) --- .../config_panel/config_panel.tsx | 55 +++------------ .../draggable_dimension_button.tsx | 15 +++- .../config_panel/layer_panel.test.tsx | 20 ++++-- .../editor_frame/config_panel/layer_panel.tsx | 32 +++++++-- .../config_panel/use_focus_update.tsx | 69 +++++++++++++++++++ .../functional/apps/lens/drag_and_drop.ts | 9 +++ .../test/functional/page_objects/lens_page.ts | 26 +++++++ 7 files changed, 168 insertions(+), 58 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/use_focus_update.tsx diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index e5b07aacee16e..393c7363dc03f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -7,7 +7,7 @@ import './config_panel.scss'; -import React, { useMemo, memo, useEffect, useState, useCallback } from 'react'; +import React, { useMemo, memo } from 'react'; import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Visualization } from '../../../types'; @@ -16,6 +16,7 @@ import { trackUiEvent } from '../../../lens_ui_telemetry'; import { generateId } from '../../../id_generator'; import { removeLayer, appendLayer } from './layer_actions'; import { ConfigPanelWrapperProps } from './types'; +import { useFocusUpdate } from './use_focus_update'; export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { const activeVisualization = props.visualizationMap[props.activeVisualizationId || '']; @@ -26,50 +27,6 @@ export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: Config ) : null; }); -function useFocusUpdate(layerIds: string[]) { - const [nextFocusedLayerId, setNextFocusedLayerId] = useState(null); - const [layerRefs, setLayersRefs] = useState>({}); - - useEffect(() => { - const focusable = nextFocusedLayerId && layerRefs[nextFocusedLayerId]; - if (focusable) { - focusable.focus(); - setNextFocusedLayerId(null); - } - }, [layerIds, layerRefs, nextFocusedLayerId]); - - const setLayerRef = useCallback((layerId, el) => { - if (el) { - setLayersRefs((refs) => ({ - ...refs, - [layerId]: el, - })); - } - }, []); - - const removeLayerRef = useCallback( - (layerId) => { - if (layerIds.length <= 1) { - return setNextFocusedLayerId(layerId); - } - - const removedLayerIndex = layerIds.findIndex((l) => l === layerId); - const nextFocusedLayerIdId = - removedLayerIndex === 0 ? layerIds[1] : layerIds[removedLayerIndex - 1]; - - setLayersRefs((refs) => { - const newLayerRefs = { ...refs }; - delete newLayerRefs[layerId]; - return newLayerRefs; - }); - return setNextFocusedLayerId(nextFocusedLayerIdId); - }, - [layerIds] - ); - - return { setNextFocusedLayerId, removeLayerRef, setLayerRef }; -} - export function LayerPanels( props: ConfigPanelWrapperProps & { activeDatasourceId: string; @@ -85,7 +42,11 @@ export function LayerPanels( } = props; const layerIds = activeVisualization.getLayerIds(visualizationState); - const { setNextFocusedLayerId, removeLayerRef, setLayerRef } = useFocusUpdate(layerIds); + const { + setNextFocusedId: setNextFocusedLayerId, + removeRef: removeLayerRef, + registerNewRef: registerNewLayerRef, + } = useFocusUpdate(layerIds); const setVisualizationState = useMemo( () => (newState: unknown) => { @@ -145,7 +106,7 @@ export function LayerPanels( void; }) { const dropType = layerDatasource.getDropTypes({ ...layerDatasourceDropProps, @@ -94,8 +96,17 @@ export function DraggableDimensionButton({ [group.accessors] ); + const registerNewButtonRefMemoized = useCallback((el) => registerNewButtonRef(columnId, el), [ + registerNewButtonRef, + columnId, + ]); + return ( -
    +
    { dispatch: jest.fn(), core: coreMock.createStart(), layerIndex: 0, - setLayerRef: jest.fn(), + registerNewLayerRef: jest.fn(), }; } @@ -620,17 +620,26 @@ describe('LayerPanel', () => { ); - - component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder'); + act(() => { + component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder'); + }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ dropType: 'reorder', droppedItem: draggingOperation, }) ); + const secondButton = component + .find(DragDrop) + .at(1) + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .instance(); + const focusedEl = document.activeElement; + expect(focusedEl).toEqual(secondButton); }); it('should copy when dropping on empty slot in the same group', () => { + (generateId as jest.Mock).mockReturnValue(`newid`); mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -657,9 +666,12 @@ describe('LayerPanel', () => { ); - component.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_in_group'); + act(() => { + component.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_in_group'); + }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ + columnId: 'newid', dropType: 'duplicate_in_group', droppedItem: draggingOperation, }) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 80e9ed05b982d..5ba73e98b42c1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -26,6 +26,7 @@ import { RemoveLayerButton } from './remove_layer_button'; import { EmptyDimensionButton } from './empty_dimension_button'; import { DimensionButton } from './dimension_button'; import { DraggableDimensionButton } from './draggable_dimension_button'; +import { useFocusUpdate } from './use_focus_update'; const initialActiveDimensionState = { isNew: false, @@ -45,7 +46,7 @@ export function LayerPanel( newVisualizationState: unknown ) => void; onRemoveLayer: () => void; - setLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; + registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; } ) { const dragDropContext = useContext(DragContext); @@ -58,7 +59,7 @@ export function LayerPanel( layerId, isOnlyLayer, onRemoveLayer, - setLayerRef, + registerNewLayerRef, layerIndex, activeVisualization, updateVisualization, @@ -70,7 +71,10 @@ export function LayerPanel( setActiveDimension(initialActiveDimensionState); }, [activeVisualization.id]); - const setLayerRefMemoized = useCallback((el) => setLayerRef(layerId, el), [layerId, setLayerRef]); + const registerLayerRef = useCallback((el) => registerNewLayerRef(layerId, el), [ + layerId, + registerNewLayerRef, + ]); const layerVisualizationConfigProps = { layerId, @@ -114,6 +118,16 @@ export function LayerPanel( const { setDimension, removeDimension } = activeVisualization; const layerDatasourceOnDrop = layerDatasource.onDrop; + const allAccessors = groups.flatMap((group) => + group.accessors.map((accessor) => accessor.columnId) + ); + + const { + setNextFocusedId: setNextFocusedButtonId, + removeRef: removeButtonRef, + registerNewRef: registerNewButtonRef, + } = useFocusUpdate(allAccessors); + const onDrop = useMemo(() => { return ( droppedItem: DragDropIdentifier, @@ -127,7 +141,12 @@ export function LayerPanel( columnId, groupId, layerId: targetLayerId, - } = (targetItem as unknown) as DraggedOperation; // TODO: correct misleading name + } = (targetItem as unknown) as DraggedOperation; + if (dropType === 'reorder' || dropType === 'field_replace' || dropType === 'field_add') { + setNextFocusedButtonId(droppedItem.id); + } else { + setNextFocusedButtonId(columnId); + } const filterOperations = groups.find(({ groupId: gId }) => gId === targetItem.groupId)?.filterOperations || @@ -171,11 +190,12 @@ export function LayerPanel( setDimension, removeDimension, layerDatasourceDropProps, + setNextFocusedButtonId, ]); return ( -
    +
    @@ -264,6 +284,7 @@ export function LayerPanel( return ( { + const focusableSelector = 'button, [href], input, select, textarea, [tabindex]'; + if (!el) { + return null; + } + if (el.matches(focusableSelector)) { + return el; + } + const firstFocusable = el.querySelector(focusableSelector); + if (!firstFocusable) { + return null; + } + return (firstFocusable as unknown) as { focus: () => void }; +}; + +type RefsById = Record; + +export function useFocusUpdate(ids: string[]) { + const [nextFocusedId, setNextFocusedId] = useState(null); + const [refsById, setRefsById] = useState({}); + + useEffect(() => { + const element = nextFocusedId && refsById[nextFocusedId]; + if (element) { + const focusable = getFirstFocusable(element); + focusable?.focus(); + setNextFocusedId(null); + } + }, [ids, refsById, nextFocusedId]); + + const registerNewRef = useCallback((id, el) => { + if (el) { + setRefsById((r) => ({ + ...r, + [id]: el, + })); + } + }, []); + + const removeRef = useCallback( + (id) => { + if (ids.length <= 1) { + return setNextFocusedId(id); + } + + const removedIndex = ids.findIndex((l) => l === id); + + setRefsById((refs) => { + const newRefsById = { ...refs }; + delete newRefsById[id]; + return newRefsById; + }); + const next = removedIndex === 0 ? ids[1] : ids[removedIndex - 1]; + return setNextFocusedId(next); + }, + [ids] + ); + + return { setNextFocusedId, removeRef, registerNewRef }; +} diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index 5b3a984f00519..a272b67de1b0a 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -143,6 +143,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( '@timestamp' ); + await PageObjects.lens.assertFocusedField('@timestamp'); }); it('should drop a field to empty dimension', async () => { await PageObjects.lens.dragFieldWithKeyboard('bytes', 4); @@ -154,12 +155,15 @@ export default function ({ getPageObjects }: FtrProviderContext) { expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') ).to.eql(['Top values of @message.raw']); + await PageObjects.lens.assertFocusedField('@message.raw'); }); it('should drop a field to an existing dimension replacing the old one', async () => { await PageObjects.lens.dragFieldWithKeyboard('clientip', 1, true); expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') ).to.eql(['Top values of clientip']); + + await PageObjects.lens.assertFocusedField('clientip'); }); it('should duplicate an element in a group', async () => { await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_yDimensionPanel', 0, 1); @@ -168,6 +172,8 @@ export default function ({ getPageObjects }: FtrProviderContext) { 'Average of bytes', 'Count of records [1]', ]); + + await PageObjects.lens.assertFocusedDimension('Count of records [1]'); }); it('should move dimension to compatible dimension', async () => { @@ -186,6 +192,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') ).to.eql([]); + await PageObjects.lens.assertFocusedDimension('@timestamp'); }); it('should move dimension to incompatible dimension', async () => { await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_yDimensionPanel', 1, 2); @@ -198,6 +205,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { 'Count of records', 'Unique count of @timestamp', ]); + await PageObjects.lens.assertFocusedDimension('Unique count of @timestamp'); }); it('should reorder elements with keyboard', async () => { await PageObjects.lens.dimensionKeyboardReorder('lnsXY_yDimensionPanel', 0, 1); @@ -205,6 +213,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { 'Unique count of @timestamp', 'Count of records', ]); + await PageObjects.lens.assertFocusedDimension('Count of records'); }); }); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index aae161ef9fcf1..add6979c2dde1 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -707,5 +707,31 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await this.saveAndReturn(); } }, + + /** + * Asserts that the focused element is a field with a specified text + * + * @param name - the element visible text + */ + async assertFocusedField(name: string) { + const input = await find.activeElement(); + const fieldAncestor = await input.findByXpath('./../../..'); + const focusedElementText = await fieldAncestor.getVisibleText(); + const dataTestSubj = await fieldAncestor.getAttribute('data-test-subj'); + expect(focusedElementText).to.eql(name); + expect(dataTestSubj).to.eql('lnsFieldListPanelField'); + }, + + /** + * Asserts that the focused element is a dimension with with a specified text + * + * @param name - the element visible text + */ + async assertFocusedDimension(name: string) { + const input = await find.activeElement(); + const fieldAncestor = await input.findByXpath('./../../..'); + const focusedElementText = await fieldAncestor.getVisibleText(); + expect(focusedElementText).to.eql(name); + }, }); } From ce441bdc3258f1f35da3c787236d14b4d133e54c Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 10 Feb 2021 13:54:52 +0100 Subject: [PATCH 100/100] RFC Improve saved object migrations algorithm (#84333) * Instead of cloning, reindex legacy index * Reindex for every v2 migration * Use _reindex?require_alias=true and a write block toggle to prevent lost deletes * Use a ..._reindex_in_progress alias so that waiting for and preventing other reindex operations is idempotent The first version of the reindex block had only the instance which was able to mark the migration as complete set and remove the write block. This means other instances couldn't know if any reindex operaitons were in progress if the migration was already marked as complete. It also meant that a failure in this critical step could result in a permanent write block. * Revert "Use a ..._reindex_in_progress alias so that waiting for and preventing other reindex operations is idempotent" This reverts commit 8baf9b13dbbe50c1dec0d4844f170304ecc0b883. * Revert "Use _reindex?require_alias=true and a write block toggle to prevent lost deletes" This reverts commit d7237ca42c4167b6931fe8c544ac7c40e27afc6c. * Use reindex + clone as a way to prevent lost deletes * Fix numbering and ignore index_not_found_exceptionfor temporary index * Apply suggestions from code review Co-authored-by: Josh Dover Co-authored-by: Josh Dover Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- rfcs/text/0013_saved_object_migrations.md | 48 ++++++++++++----------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/rfcs/text/0013_saved_object_migrations.md b/rfcs/text/0013_saved_object_migrations.md index 6f5ab280a4612..88879e5e706eb 100644 --- a/rfcs/text/0013_saved_object_migrations.md +++ b/rfcs/text/0013_saved_object_migrations.md @@ -248,45 +248,49 @@ Note: 6. Use the reindexed legacy `.kibana_pre6.5.0_001` as the source for the rest of the migration algorithm. 3. If `.kibana` and `.kibana_7.10.0` both exists and are pointing to the same index this version's migration has already been completed. 1. Because the same version can have plugins enabled at any point in time, - perform the mappings update in step (7) and migrate outdated documents - with step (8). - 2. Skip to step (10) to start serving traffic. + migrate outdated documents with step (9) and perform the mappings update in step (10). + 2. Skip to step (12) to start serving traffic. 4. Fail the migration if: 1. `.kibana` is pointing to an index that belongs to a later version of Kibana .e.g. `.kibana_7.12.0_001` 2. (Only in 8.x) The source index contains documents that belong to an unknown Saved Object type (from a disabled plugin). Log an error explaining that the plugin that created these documents needs to be enabled again or that these objects should be deleted. See section (4.2.1.4). -5. Mark the source index as read-only and wait for all in-flight operations to drain (requires https://github.com/elastic/elasticsearch/pull/58094). This prevents any further writes from outdated nodes. Assuming this API is similar to the existing `//_close` API, we expect to receive `"acknowledged" : true` and `"shards_acknowledged" : true`. If all shards don’t acknowledge within the timeout, retry the operation until it succeeds. -6. Clone the source index into a new target index which has writes enabled. All nodes on the same version will use the same fixed index name e.g. `.kibana_7.10.0_001`. The `001` postfix isn't used by Kibana, but allows for re-indexing an index should this be required by an Elasticsearch upgrade. E.g. re-index `.kibana_7.10.0_001` into `.kibana_7.10.0_002` and point the `.kibana_7.10.0` alias to `.kibana_7.10.0_002`. - 1. `POST /.kibana_n/_clone/.kibana_7.10.0_001?wait_for_active_shards=all {"settings": {"index.blocks.write": false}}`. Ignore errors if the clone already exists. - 2. Wait for the cloning to complete `GET /_cluster/health/.kibana_7.10.0_001?wait_for_status=green&timeout=60s` If cloning doesn’t complete within the 60s timeout, log a warning for visibility and poll again. -7. Update the mappings of the target index +5. Set a write block on the source index. This prevents any further writes from outdated nodes. +6. Create a new temporary index `.kibana_7.10.0_reindex_temp` with `dynamic: false` on the top-level mappings so that any kind of document can be written to the index. This allows us to write untransformed documents to the index which might have fields which have been removed from the latest mappings defined by the plugin. Define minimal mappings for the `migrationVersion` and `type` fields so that we're still able to search for outdated documents that need to be transformed. + 1. Ignore errors if the target index already exists. +7. Reindex the source index into the new temporary index. + 1. Use `op_type=create` `conflicts=proceed` and `wait_for_completion=false` so that multiple instances can perform the reindex in parallel but only one write per document will succeed. + 2. Wait for the reindex task to complete. If reindexing doesn’t complete within the 60s timeout, log a warning for visibility and poll again. +8. Clone the temporary index into the target index `.kibana_7.10.0_001`. Since any further writes will only happen against the cloned target index this prevents a lost delete from occuring where one instance finishes the migration and deletes a document and another instance's reindex operation re-creates the deleted document. + 1. Set a write block on the temporary index + 2. Clone the temporary index into the target index while specifying that the target index should have writes enabled. + 3. If the clone operation fails because the target index already exist, ignore the error and wait for the target index to become green before proceeding. + 4. (The `001` postfix in the target index name isn't used by Kibana, but allows for re-indexing an index should this be required by an Elasticsearch upgrade. E.g. re-index `.kibana_7.10.0_001` into `.kibana_7.10.0_002` and point the `.kibana_7.10.0` alias to `.kibana_7.10.0_002`.) +9. Transform documents by reading batches of outdated documents from the target index then transforming and updating them with optimistic concurrency control. + 1. Ignore any version conflict errors. + 2. If a document transform throws an exception, add the document to a failure list and continue trying to transform all other documents. If any failures occured, log the complete list of documents that failed to transform. Fail the migration. +10. Update the mappings of the target index 1. Retrieve the existing mappings including the `migrationMappingPropertyHashes` metadata. - 2. Update the mappings with `PUT /.kibana_7.10.0_001/_mapping`. The API deeply merges any updates so this won't remove the mappings of any plugins that were enabled in a previous version but are now disabled. + 2. Update the mappings with `PUT /.kibana_7.10.0_001/_mapping`. The API deeply merges any updates so this won't remove the mappings of any plugins that are disabled on this instance but have been enabled on another instance that also migrated this index. 3. Ensure that fields are correctly indexed using the target index's latest mappings `POST /.kibana_7.10.0_001/_update_by_query?conflicts=proceed`. In the future we could optimize this query by only targeting documents: 1. That belong to a known saved object type. - 2. Which don't have outdated migrationVersion numbers since these will be transformed anyway. - 3. That belong to a type whose mappings were changed by comparing the `migrationMappingPropertyHashes`. (Metadata, unlike the mappings isn't commutative, so there is a small chance that the metadata hashes do not accurately reflect the latest mappings, however, this will just result in an less efficient query). -8. Transform documents by reading batches of outdated documents from the target index then transforming and updating them with optimistic concurrency control. - 1. Ignore any version conflict errors. - 2. If a document transform throws an exception, add the document to a failure list and continue trying to transform all other documents. If any failures occured, log the complete list of documents that failed to transform. Fail the migration. -9. Mark the migration as complete. This is done as a single atomic +11. Mark the migration as complete. This is done as a single atomic operation (requires https://github.com/elastic/elasticsearch/pull/58100) - to guarantees when multiple versions of Kibana are performing the + to guarantee that when multiple versions of Kibana are performing the migration in parallel, only one version will win. E.g. if 7.11 and 7.12 are started in parallel and migrate from a 7.9 index, either 7.11 or 7.12 should succeed and accept writes, but not both. - 3. Checks that `.kibana` alias is still pointing to the source index - 4. Points the `.kibana_7.10.0` and `.kibana` aliases to the target index. - 5. If this fails with a "required alias [.kibana] does not exist" error fetch `.kibana` again: + 1. Check that `.kibana` alias is still pointing to the source index + 2. Point the `.kibana_7.10.0` and `.kibana` aliases to the target index. + 3. Remove the temporary index `.kibana_7.10.0_reindex_temp` + 4. If this fails with a "required alias [.kibana] does not exist" error or "index_not_found_exception" for the temporary index, fetch `.kibana` again: 1. If `.kibana` is _not_ pointing to our target index fail the migration. - 2. If `.kibana` is pointing to our target index the migration has succeeded and we can proceed to step (10). -10. Start serving traffic. All saved object reads/writes happen through the + 2. If `.kibana` is pointing to our target index the migration has succeeded and we can proceed to step (12). +12. Start serving traffic. All saved object reads/writes happen through the version-specific alias `.kibana_7.10.0`. Together with the limitations, this algorithm ensures that migrations are idempotent. If two nodes are started simultaneously, both of them will start transforming documents in that version's target index, but because migrations are idempotent, it doesn’t matter which node’s writes win. - #### Known weaknesses: (Also present in our existing migration algorithm since v7.4) When the task manager index gets reindexed a reindex script is applied.